Оскільки ви запитали, чому C # зробив це таким чином, найкраще запитати у творців C #. Андер Хейлсберг, провідний архітектор C #, відповів, чому вони в інтерв'ю вирішили не користуватися віртуальною програмою (як на Java) , відповідні фрагменти нижче.
Майте на увазі, що Java за замовчуванням має віртуальне кінцеве ключове слово для позначення методу як невіртуального. Ще два поняття, які слід вивчити, але багато людей не знають про остаточне ключове слово або не використовують його активно. C # змушує використовувати віртуальне та нове / переосмислювати свідомо приймати ці рішення.
Причин кілька. Один - це продуктивність . Ми можемо спостерігати, що коли люди пишуть код на Java, вони забувають позначити свої методи остаточними. Тому ці методи є віртуальними. Оскільки вони віртуальні, вони також не дуже ефективні. Є просто накладні витрати, пов'язані з тим, що це віртуальний метод. Це одне питання.
Більш важливим питанням є версія . Існують дві школи думок щодо віртуальних методів. Академічна школа думок говорить: "Все повинно бути віртуальним, тому що я, можливо, захочу його перемогти коли-небудь". Прагматична школа думки, яка походить від побудови реальних додатків, що працюють в реальному світі, говорить: "Ми повинні бути реальними уважними щодо того, що ми робимо віртуальними".
Коли ми робимо щось віртуальне на платформі, ми даємо дуже багато обіцянок щодо того, як це розвиватиметься в майбутньому. Що стосується невіртуального методу, ми обіцяємо, що при виклику цього методу відбудеться х і у. Коли ми публікуємо віртуальний метод в API, ми не тільки обіцяємо, що при виклику цього методу, x і y відбудуться. Ми також обіцяємо, що коли ви скасуєте цей метод, ми назвемо його в цій певній послідовності стосовно цих інших, і держава буде в цій і тій інваріантній.
Кожен раз, коли ви говорите віртуальний в API, ви створюєте гачок зворотного дзвінка. Як розробник ОС або API, ви повинні бути ретельно обережними щодо цього. Ви не хочете, щоб користувачі переосмислювали та підключали будь-яку довільну точку API, тому що ви не можете обов'язково давати ці обіцянки. І люди можуть не повністю зрозуміти обіцянки, які вони дають, коли роблять щось віртуальне.
В інтерв’ю є більше дискусій про те, як розробники думають про дизайн спадкування класів, і як це призвело до їх рішення.
Тепер до наступного питання:
Я не в змозі зрозуміти, чому в світі я збираюся додати метод у свій DerivedClass з тим же ім'ям і такою ж підписом, як BaseClass, і визначити нову поведінку, але під час поліморфізму під час виконання буде застосовано метод BaseClass! (що не переважає, але логічно повинно бути).
Це було б тоді, коли похідний клас хоче заявити, що він не дотримується договору базового класу, але має метод з такою ж назвою. (Для всіх, хто не знає різниці між new
і override
в C #, дивіться цю сторінку MSDN ).
Дуже практичний сценарій такий:
- Ви створили API, в якому є клас під назвою
Vehicle
.
- Я почав використовувати ваш API і отримав
Vehicle
.
- У вашому
Vehicle
класі не було жодного методу PerformEngineCheck()
.
- У своєму
Car
класі я додаю метод PerformEngineCheck()
.
- Ви випустили нову версію свого API та додали
PerformEngineCheck()
.
- Я не можу перейменувати свій метод, оскільки мої клієнти залежать від мого API, і він би порушив їх.
Тому коли я перекомпілюю ваш новий API, C # попереджає мене про цю проблему, наприклад
Якщо бази PerformEngineCheck()
не було virtual
:
app2.cs(15,17): warning CS0108: 'Car.PerformEngineCheck()' hides inherited member 'Vehicle.PerformEngineCheck()'.
Use the new keyword if hiding was intended.
А якщо база PerformEngineCheck()
була virtual
:
app2.cs(15,17): warning CS0114: 'Car.PerformEngineCheck()' hides inherited member 'Vehicle.PerformEngineCheck()'.
To make the current member override that implementation, add the override keyword. Otherwise add the new keyword.
Тепер я повинен чітко прийняти рішення, чи дійсно мій клас подовжує контракт базового класу, чи це інший контракт, але буває, що це одне й те саме ім'я.
- Роблячи це
new
, я не ламаю своїх клієнтів, якщо функціональність базового методу відрізнялася від похідного методу. Будь-який код, на який посилається Vehicle
, не побачить Car.PerformEngineCheck()
виклику, але код, на який було посилання, Car
і надалі буде бачити той самий функціонал, який я запропонував PerformEngineCheck()
.
Аналогічний приклад - коли інший метод базового класу може викликати виклик PerformEngineCheck()
(особливо в новій версії), як завадити йому викликати PerformEngineCheck()
похідний клас? У Java це рішення лежало б на базовому класі, але воно не знає нічого про похідний клас. У C # це рішення покладається як на базовий клас (за допомогою virtual
ключового слова), так і на похідний клас (через new
і override
ключові слова).
Звичайно, помилки, які випускає компілятор, також служать корисним інструментом для програмістів, щоб несподівано не помилитися (тобто або переосмислити, або надати нову функціональність, не усвідомлюючи цього.)
Як сказав Андерс, реальний світ змушує нас до таких питань, які, якби ми починали з нуля, ми ніколи не хотіли б вступати.
EDIT: Додано приклад того, де new
потрібно було б використовувати для забезпечення сумісності інтерфейсу.
EDIT: Під час обговорення коментарів я також натрапив на написання Еріка Ліпперта (тоді одного з членів комітету дизайну C #) щодо інших прикладних сценаріїв (згаданих Брайаном).
ЧАСТИНА 2: На основі оновленого питання
Але у випадку C #, якщо SpaceShip не перевищує клас автомобіля, «пришвидшити та використовувати новий, то логіка мого коду буде порушена. Це не недолік?
Хто вирішує, чи SpaceShip
насправді переважає, Vehicle.accelerate()
чи відрізняється? Це має бути SpaceShip
розробником. Тож якщо SpaceShip
розробник вирішить, що вони не дотримуються контракту базового класу, то ваш виклик не Vehicle.accelerate()
повинен надходити SpaceShip.accelerate()
, чи повинен? Саме тоді вони позначають це як new
. Однак якщо вони вирішать, що він дійсно зберігає контракт, то вони насправді його позначать override
. У будь-якому випадку ваш код буде поводитись правильно, викликаючи правильний метод на основі контракту . Як ваш код може вирішити, чи SpaceShip.accelerate()
насправді це переважає Vehicle.accelerate()
чи це зіткнення імені? (Дивіться мій приклад вище).
Однак, в разі неявного успадкування, навіть якщо SpaceShip.accelerate()
не тримати контракт на Vehicle.accelerate()
виклик методу буде по- , як і раніше йти SpaceShip.accelerate()
.