Почему C # по умолчанию реализует методы как не виртуальные?

105

В отличие от Java, почему C # по умолчанию рассматривает методы как невиртуальные функции? Это скорее проблема производительности, чем другие возможные результаты?

Мне вспоминается абзац Андерса Хейлсберга о некоторых преимуществах, которые дает существующая архитектура. Но как насчет побочных эффектов? Действительно ли наличие не виртуальных методов по умолчанию - хороший компромисс?

Бурку Доган
источник
1
Ответы, в которых упоминаются причины производительности, упускают из виду тот факт, что компилятор C # в основном компилирует вызовы методов в callvirt, а не в call . Вот почему в C # невозможно иметь метод, который ведет себя иначе, если thisссылка равна нулю. См. Здесь для получения дополнительной информации.
Энди
Правда! Инструкция call IL в основном предназначена для вызовов статических методов.
RBT
1
Мысли архитектора C # Андерса Хейлсберга здесь и здесь .
RBT

Ответы:

98

Классы должны быть разработаны с учетом наследования, чтобы можно было использовать его преимущества. Наличие методов virtualпо умолчанию означает, что каждая функция в классе может быть отключена и заменена другой, что на самом деле не очень хорошо. Многие даже считают, что занятия должны были быть sealedпо умолчанию.

virtualметоды также могут иметь небольшое влияние на производительность. Однако вряд ли это основная причина.

Мехрдад Афшари
источник
7
Лично я сомневаюсь, что это касается производительности. Даже для виртуальных функций компилятор очень способен понять, где заменить callvirtпростым callв коде IL (или даже дальше вниз по течению при JITting). Java HotSpot делает то же самое. В остальном ответ точен.
Конрад Рудольф
5
Я согласен, что производительность не стоит на первом месте, но она также может сказаться на производительности. Наверное, очень мало. Конечно, это не повод для такого выбора. Просто хотел упомянуть об этом.
Мехрдад Афшари,
71
Запечатанные классы вызывают у меня желание бить младенцев.
mxmissile
6
@mxmissile: я тоже, но хороший дизайн API все равно сложен. Я думаю, что запечатанный по умолчанию имеет смысл для вашего кода. Чаще всего другой программист в вашей команде не рассматривал наследование при реализации нужного вам класса, и запечатанный по умолчанию хорошо передавал это сообщение.
Роман Старков
49
Многие люди тоже психопаты, это не значит, что мы должны их слушать. Virtual должен быть значением по умолчанию в C #, код должен быть расширяемым без необходимости изменять реализации или полностью переписывать его. Люди, которые чрезмерно защищают свои API, часто заканчивают тем, что API не используются или используются наполовину, что намного хуже, чем сценарий злоупотребления ими.
Chris Nicola
89

Я удивлен, что здесь, кажется, существует такой консенсус, что не виртуальный по умолчанию - правильный способ делать что-то. Я собираюсь перейти на другую - я думаю, прагматичную - сторону забора.

Большинство оправданий мне напоминают старый аргумент «Если мы дадим вам силу, вы можете навредить себе». От программистов ?!

Мне кажется, что кодер, который не знал достаточно (или не имел достаточно времени), чтобы спроектировать свою библиотеку для наследования и / или расширяемости, - это кодер, который произвел именно ту библиотеку, которую мне, вероятно, придется исправить или настроить - именно та библиотека, в которой возможность переопределения будет наиболее полезной.

Количество раз, когда мне приходилось писать уродливый, отчаянный обходной код (или отказываться от использования и откатывать собственное альтернативное решение), потому что я не могу переопределить далеко, намного превышает количество раз, когда меня когда-либо укусили ( например, в Java), переопределив то, что дизайнер мог не учитывать.

Не виртуальный по умолчанию усложняет мне жизнь.

ОБНОВЛЕНИЕ: [совершенно правильно] было указано, что я на самом деле не ответил на вопрос. Итак - и с извинениями за опоздание ....

Я как бы хотел написать что-нибудь содержательное, вроде «C # по умолчанию реализует методы как не виртуальные, потому что было принято плохое решение, которое ценило программы выше, чем программисты». (Я думаю, что это может быть в какой-то мере оправдано, основываясь на некоторых других ответах на этот вопрос - например, о производительности (преждевременная оптимизация, кто угодно?) Или гарантиях поведения классов.)

Однако я понимаю, что просто изложу свое мнение, а не тот окончательный ответ, которого желает Stack Overflow. Конечно, подумал я, на самом высоком уровне окончательный (но бесполезный) ответ таков:

По умолчанию они не виртуальные, потому что разработчики языков должны были принять решение, и они выбрали именно это.

Теперь я предполагаю, что точную причину, по которой они приняли это решение, мы никогда не… ох, подождите! Стенограмма разговора!

Таким образом, может показаться, что ответы и комментарии здесь об опасностях переопределения API и необходимости явного проектирования для наследования находятся на правильном пути, но все они не имеют важного временного аспекта: основная забота Андерса заключалась в поддержании неявных классов или API контракт между версиями . И я думаю, что на самом деле он больше озабочен тем, чтобы позволить платформе .Net / C # изменяться под кодом, чем беспокоиться об изменении пользовательского кода поверх платформы. (И его «прагматическая» точка зрения прямо противоположна моей, потому что он смотрит с другой стороны.)

(Но разве они не могли просто выбрать виртуальный вариант по умолчанию, а затем добавить «final» в кодовую базу? Возможно, это не совсем то же самое ... и Андерс явно умнее меня, так что я позволю этому врать.)

Mwardm
источник
2
Не могу больше согласиться. Очень неприятно, когда вы используете сторонний API и хотите отменить какое-то поведение, но не можете.
Энди
3
Я с энтузиазмом согласен. Если вы собираетесь опубликовать API (будь то внутри компании или для внешнего мира), вы действительно можете и должны убедиться, что ваш код предназначен для наследования. Если вы публикуете его для использования многими людьми, вы должны делать его качественным и отточенным. По сравнению с усилиями, необходимыми для хорошего общего дизайна (хороший контент, ясные варианты использования, тестирование, документация), проектирование с учетом наследования действительно не так уж и плохо. А если вы не публикуете, виртуальный по умолчанию занимает меньше времени, и вы всегда можете исправить небольшую часть случаев, когда это проблематично.
Gravity
2
Если бы в Visual Studio была только функция редактора, автоматически помечающая все методы / свойства как virtual... Надстройка Visual Studio для кого-нибудь?
kevinarpe
1
Хотя я полностью согласен с вами, это не совсем ответ.
Крис Морган
2
Учитывая современные методы разработки, такие как внедрение зависимостей, имитирующие фреймворки и ORM, кажется очевидным, что наши разработчики C # немного упустили цель. Очень неприятно тестировать зависимость, когда вы не можете переопределить свойство по умолчанию.
Джереми Головач
17

Потому что слишком легко забыть, что метод может быть переопределен, а не разработан для этого. C # заставляет задуматься, прежде чем сделать его виртуальным. Считаю это отличным дизайнерским решением. Некоторые люди (например, Джон Скит) даже сказали, что классы должны быть запечатаны по умолчанию.

Зифре
источник
12

Подводя итог тому, что говорили другие, есть несколько причин:

1. В C # есть много вещей в синтаксисе и семантике, которые взяты прямо из C ++. Тот факт, что методы, которые в C ++ по умолчанию не были виртуальными, повлияли на C #.

2- Наличие виртуального каждого метода по умолчанию является проблемой производительности, потому что каждый вызов метода должен использовать виртуальную таблицу объекта. Более того, это сильно ограничивает способность компилятора Just-In-Time встраивать методы и выполнять другие виды оптимизации.

3- Самое главное, что если методы не являются виртуальными по умолчанию, вы можете гарантировать поведение своих классов. Когда они по умолчанию виртуальные, например, в Java, вы даже не можете гарантировать, что простой метод получения будет работать так, как задумано, потому что его можно переопределить для выполнения каких-либо действий в производном классе (конечно, вы можете и должны сделать метод и / или финал класса).

Как упоминал Зифре, можно задаться вопросом, почему язык C # не пошел дальше и не сделал классы запечатанными по умолчанию. Это часть всей дискуссии о проблемах наследования реализации, которая является очень интересной темой.

Trillian
источник
1
Я бы тоже предпочел классы, запечатанные по умолчанию. Тогда это было бы последовательным.
Энди
9

C # находится под влиянием C ++ (и других). C ++ по умолчанию не поддерживает динамическую отправку (виртуальные функции). Одним из (хороших?) Аргументов в пользу этого является вопрос: «Как часто вы реализуете классы, являющиеся членами иерархии классов?». Еще одна причина не включать динамическую отправку по умолчанию - это объем памяти. Класс без виртуального указателя (vpointer), указывающего на виртуальную таблицу , конечно, меньше, чем соответствующий класс с включенной поздней привязкой.

По поводу производительности не так-то просто сказать «да» или «нет». Причиной этого является компиляция Just In Time (JIT), которая является оптимизацией времени выполнения в C #.

Другой, аналогичный вопрос про " скорость виртуальных звонков .. "

Шильдмейер
источник
Я немного сомневаюсь, что виртуальные методы влияют на производительность C # из-за JIT-компилятора. Это одна из областей, где JIT может быть лучше, чем автономная компиляция, потому что они могут встроить вызовы функций, которые «неизвестны» до выполнения
Дэвид Курнапо
1
На самом деле я думаю, что на него больше повлияла Java, чем C ++, который делает это по умолчанию.
Mehrdad Afshari,
5

Простая причина - это затраты на дизайн и обслуживание в дополнение к затратам на производительность. Виртуальный метод имеет дополнительную стоимость по сравнению с невиртуальным методом, потому что разработчик класса должен спланировать, что происходит, когда метод переопределяется другим классом. Это имеет большое влияние, если вы ожидаете, что конкретный метод обновит внутреннее состояние или будет иметь определенное поведение. Теперь вы должны спланировать, что произойдет, когда производный класс изменит это поведение. В такой ситуации гораздо сложнее написать надежный код.

С невиртуальным методом у вас есть полный контроль. Во всем, что идет не так, виноват первоначальный автор. О коде гораздо легче думать.

ДжаредПар
источник
Это действительно старый пост, но для новичка, который пишет свой первый проект, я постоянно беспокоюсь о непредвиденных / неизвестных последствиях написанного мной кода. Очень приятно знать, что не виртуальные методы полностью МОЯ вина.
Trevorc
2

Если бы все методы C # были виртуальными, тогда vtbl был бы намного больше.

У объектов C # есть виртуальные методы, только если в классе определены виртуальные методы. Это правда, что все объекты имеют информацию о типе, которая включает эквивалент vtbl, но если виртуальные методы не определены, будут присутствовать только базовые методы Object.

@Tom Hawtin: Возможно, правильнее будет сказать, что C ++, C # и Java - все из семейства языков C :)

Томас Братт
источник
1
Почему для vtable должно быть проблемой быть намного больше? На класс (а не на экземпляр) есть только одна виртуальная таблица, поэтому ее размер не имеет большого значения.
Gravity
1

Исходя из опыта работы с Perl, я думаю, что C # предопределил гибель каждого разработчика, который, возможно, захотел расширить и изменить поведение базового класса с помощью невиртуального метода, не заставляя всех пользователей нового класса знать о потенциально скрытых проблемах. подробности.

Рассмотрим метод Add класса List. Что, если разработчик захочет обновить одну из нескольких потенциальных баз данных всякий раз, когда конкретный список «добавлен»? Если бы «Добавить» было виртуальным по умолчанию, разработчик мог бы разработать класс «BackedList», который заменял бы метод «Добавить», не заставляя весь клиентский код знать, что это «BackedList» вместо обычного «списка». Для всех практических целей «BackedList» можно рассматривать как просто еще один «список» из клиентского кода.

Это имеет смысл с точки зрения большого основного класса, который может предоставлять доступ к одному или нескольким компонентам списка, которые сами поддерживаются одной или несколькими схемами в базе данных. Учитывая, что методы C # не являются виртуальными по умолчанию, список, предоставляемый основным классом, не может быть простым IEnumerable, ICollection или даже экземпляром List, но вместо этого должен быть объявлен клиенту как BackedList, чтобы гарантировать, что новая версия операции «Добавить» вызывается для обновления правильной схемы.

Трэвис
источник
Да, виртуальные методы по умолчанию облегчают работу, но есть ли в этом смысл? Вы можете использовать множество вещей, чтобы облегчить жизнь. Почему не только публичные поля в классах? Изменить его поведение слишком легко. На мой взгляд, все в языке должно быть строго инкапсулировано, жестким и устойчивым к изменениям по умолчанию. Меняйте его только при необходимости. Просто философские решения относительно дизайна. Продолжение ..
nawfal
... Чтобы поговорить на текущую тему, модель наследования следует использовать, только если Bесть A. Если Bтребуется что-то другое, Aто это не так A. Я считаю, что возможность переопределения в самом языке является недостатком дизайна. Если вам нужен другой Addметод, тогда ваш класс коллекции не является List. Пытаться сказать, что это подделка. Правильный подход здесь - композиция (а не подделка). Правда, вся структура построена на переопределяющих возможностях, но мне это просто не нравится.
nawfal
Думаю, я понимаю вашу точку зрения, но в приведенном конкретном примере: «BackedList» может просто реализовать интерфейс «IList», а клиент знает только об интерфейсе. право? я что-то упускаю? однако я понимаю более широкую мысль, которую вы пытаетесь донести.
Ветрас,
0

Конечно, это не проблема производительности. Интерпретатор Java Sun использует один и тот же код для отправки ( invokevirtualбайт-код), а HotSpot генерирует один и тот же код независимо от того, генерирует он finalили нет. Я считаю, что все объекты C # (но не структуры) имеют виртуальные методы, поэтому вам всегда понадобится vtblидентификация класса / runtime. C # - это диалект «Java-подобных языков». Предполагать, что это исходит от C ++, не совсем честно.

Есть идея, что вы должны «спроектировать наследование или запретить его». Это звучит как отличная идея до того момента, когда у вас возникнет серьезное экономическое обоснование, которое нужно быстро исправить. Возможно, наследование от кода, который вы не контролируете.

Том Хотин - tackline
источник
Hotspot вынужден пойти на многое, чтобы выполнить эту оптимизацию именно потому, что все методы по умолчанию являются виртуальными, что оказывает огромное влияние на производительность. CoreCLR может достичь аналогичной производительности, но при этом намного проще
Яир Хальберштадт
@YairHalberstadt Hotspot должна иметь возможность откатить скомпилированный код по ряду других причин. Прошло много лет с тех пор, как я посмотрел на исходный код, но разница между finalэффективными finalметодами тривиальна. Также стоит отметить, что он может выполнять биморфное встраивание, то есть встроенные методы с двумя разными реализациями.
Tom Hawtin - tackline