[Перевод] Исчерпывающий список различий между VB.NET и C#. Часть 1

image

Согласно рейтингу TIOBE в 2018 году VB.NET обогнал по популярности C#. Совпадение или нет, но в феврале Эрик Липперт, один из создателей C#, призвал читателей обратить внимание на блог его друга, бывшего коллеги по команде компилятора Roslyn и, по совместительству, ярого фаната VB.NET, Энтони Грина. «Подобные ресурсы — это глубинные детали от экспертов, которые не так легко найти, читая документацию», пишет Эрик. Представляем вашему вниманию первую часть перевода статьи Энтони Грина «Исчерпывающий список различий между VB.NET и C#». Возможно, именно в этих различиях кроется секрет динамики рейтинга этих языков.
За без малого полжизни я был свидетелем и участником бесчисленных дискуссий о том, насколько похожи или отличаются два самых популярных языка .NET. Сперва как любитель, затем как профессионал и, наконец, как защитник клиентов (customer advocate), программный менеджер и дизайнер языков, я без преувеличения могу сказать, сколько раз я слышал или читал что-то типа:

»… VB.NET — это на самом деле просто тонкий слой поверх IL, как C#…»

или 

»… VB.NET на самом деле просто C# без точек с запятой …»

Как если бы языки были XML-преобразованием или таблицей стилей.

И если некий пылкий посетитель не пишет это в комменте, то часто это подразумевается в вопросах вроде: «Привет, Энтони! Я столкнулся вот с таким небольшим различием в одном единственном месте — это баг? Как могли эти два в остальном идентичных языка, которые и должны быть идентичны во имя всего доброго и святого в этом мире, разойтись в одном этом месте? За что нам такая несправедливость?!

»Разойтись», как будто они были одинаковыми, пока не произошла мутация, а затем стали отдельными видами. ХА!

Но мне это понятно. До того, как я присоединился к Microsoft, я, возможно, тоже смутно придерживался этой идеи и использовал ее в качестве аргумента, чтобы ответить противникам или кого-то успокоить. Я понимаю ее очарование. Ее легко понять и очень легко повторять. Но работая над Roslyn (по сути переписывание VB и C# полностью с нуля) в течение 5 лет, я понял, насколько однозначно ложной является эта идея. Я работал с командой разработчиков и тестировщиков, чтобы реализовать заново каждый дюйм обоих языков, а также их инструментарий в огромном многопроектном солюшене с миллионами строк кода, написанными на обоих языках. И с учетом большого количества разработчиков, переключающихся между ними туда и обратно, и высокой планки совместимости с результатами и опытом предыдущих версий, а также необходимостью достоверно воспроизвести в мельчайших деталях гигантский объем API, я был вынужден очень близко познакомиться с различиями. На самом деле, иногда мне казалось, что я узнаю что-то новое о VB.NET (мой любимый язык) каждый день.

И вот, наконец, я нашел время, чтобы засесть и выгрузить из мозга частицу того, что я узнал, используя и создавая VB.NET за последние 15 лет, в надежде, что я смогу как минимум сэкономить в следующий раз свое время.

Прежде чем перейти к списку, я изложу основные правила:

  • Этот список не является исчерпывающим в обычном смысле. Он исчерпывающий силы. Это не все существующие различия. Это даже не все различия, которые я вообще знаю. Это просто различия, которые я могу вспомнить первыми, пока не слишком устану, чтобы продолжать; пока не исчерпаю силы. Если же я или кто-то из вас встретит или вспомнит другие различия, я с удовольствием обновлю этот список.
  • Я начну с начала спецификации VB 11 и двинусь вниз, используя ее содержание, чтобы напомнить себе о различиях, которые приходят в голову по этой теме первыми.
  • Это НЕ список функций в VB, которых нет в C#. Так что никаких «XML-литералы против указателей». Это слишком банально, и уже есть тонны таких списков в Интернете (некоторые из которых были написаны мной, и, возможно, в будущем я напишу еще). Я сфокусируюсь прежде всего на конструкциях, имеющих аналог в обоих языках, и где неосведомленный наблюдатель может предположить, что эти две вещи ведут себя одинаково, но где есть маленькие или большие различия; они могут одинаково выглядеть, но по-разному работать или генерировать в конечном счете разный код.
  • Это НЕ список синтаксических различий между VB и C# (которых бесчисленное множество). Я буду в основном говорить о семантических различиях (что вещи означают), а не о синтаксических (как вещи пишутся). Так что никаких штук типа «VB начинает комментарии с ', а C# использует //» или «в C# _ является допустимым идентификатором, но не в VB». Но я нарушу это правило для нескольких случаев. В конце концов, первый раздел спецификации посвящен лексическим правилам.
  • Довольно часто я буду приводить примеры, а иногда буду предлагать обоснования, почему дизайн мог пойти тем или иным путем. Некоторые решения по дизайну принимались на моих глазах, но подавляющее большинство предшествовало моему времени, и я могу только догадываться, почему они были приняты.
  • Пожалуйста, оставьте комментарий или напишите мне в Твиттере (@ThatVBGuy), чтобы сообщить мне ваши любимые отличия и/или те, о которых вы хотели бы узнать поглубже.


Определившись с ожиданиями и без дальнейших задержек…

Содержание


Скрытый текст

Синтаксис и препроцессинг


Объявления и пр.


  • 5. VB иногда пропускает объявления implements в IL для предотвращения случайной неявной реализации интерфейса по имени
  • 6. VB по умолчанию скрывает члены базового класса по имени (Shadows), а не по имени и сигнатуре (Overloads)
  • 7. VB11 и ниже являются более строгими к Protected-членам в дженериках
  • 8. Синтаксис «именованный аргумент (Named argument)» в атрибутах всегда инициализирует свойства / поля
  • 9. Все объявления верхнего уровня (обычно) неявно находятся в корневом пространстве имен проекта.
  • 10. Модули не генерируются как запечатанные (sealed) абстрактные классы в IL, поэтому они не похожи в точности на статические классы C# и наоборот
  • 11. Вам не нужен явный метод для точки входа (Sub Main) в приложениях WinForms
  • 12. Если вы вызываете некоторые устаревшие методы рантайма VB (например, FileOpen), вызывающий метод будет неявно помечен атрибутом, чтобы отключить инлайнинг из соображений корректности
  • 13. Если ваш тип помечен атрибутом DesignerGenerated и не содержит каких-либо явных объявлений конструктора, то генерируемый компилятором по умолчанию вызовет InitializeComponent, если он определен для этого типа.
  • 14. Отсутствие модификатора Partial НЕ означает, что тип не является partial
  • 15. В классах по умолчанию уровень доступа Public для всего, кроме полей, а в структурах Public и для полей тоже
  • 16. VB инициализирует поля ПОСЛЕ вызова базового конструктора, тогда как C # инициализирует их ДО вызова базового конструктора
  • 17. Неявно объявленное вспомогательное поле (backing field) событий VB имеет другое имя, нежели в C#, и доступно по имени
  • 18. Неявно объявленное вспомогательное поле авто-свойств VB имеет обычное имя и доступно по этому имени
  • 19. Неявно объявленное вспомогательное поле read-only авто-свойств позволяет запись
  • 20. Атрибуты событий иногда применяются к вспомогательному полю событий

Инструкции



Синтаксис и препроцессинг


1. Ключевые слова и операторы VB могут использовать полноширинные (full-width) символы


В некоторых языках (не знаю, в скольких точно, но как минимум в некоторых формах китайского, японского и корейского) используются полноширинные символы. Вкратце это означает, что при использовании моноширинного шрифта (как это делают большинство программистов) китайский символ будет занимать в два раза больше места по горизонтали, чем латинские символы, которые мы привыкли видеть на западе. Например:

wlj80uri5nr810dqgpwymj5eru0.png

Здесь у меня объявление переменной, написанное на японском языке, и инициализация строкой, также написанной на японском языке. Согласно переводчику Bing, переменная называется «greeting», а в строке написано «Hello World!». Имя переменной на японском составляет всего 2 символа, но оно занимает пространство из 4 символов половинной ширины, которые обычно выдает моя клавиатура, как демонстрирует первый комментарий. Существуют полноширинные версии чисел и всех других печатных ASCII-символов, имеющие ту же ширину, что и японские. Чтобы продемонстрировать это, я написал второй комментарий, используя полноширинные числа »1» и »2». Это не такие »1» и »2», как в первом комментарии. Между числами нет пробелов. Вы также можете видеть, что по размеру символы — не ровно 2 символа в ширину, там есть небольшое смещение. Частично это происходит потому, что эта программа смешивает полноширинные и полуширинные символы в одной строке и во всех трех строках.

Пробелы имеют половинную ширину, буквенно-цифровые символы — полную ширину. Мы не программисты, если не помешаны на выравнивании текста. И мне кажется, что если вы китаец, японец или кореец (или кто-то еще, кто использует полноразмерные символы для своего языка) и используете идентификаторы или строки, написанные на родном языке, эти мелкие ошибки выравнивания приводят в бешенство.

Насколько я понимаю, в зависимости от вашей японской клавиатуры переключаться между иероглифами и латынью легко, но предпочтительнее использовать полноширинные латинские символы. VB поддерживает это в ключевых словах, пробелах, операторах и даже кавычках. Так что все это можно записать так:

memqph465sxyx_tvxov6fglrjfw.png

Как видите, в этой версии ключевые слова, пробелы, комментарии, операторы, даже кавычки используют свои полноразмерные версии. В хаос внесен порядок.

Да. Японцы используют VB. На самом деле, несмотря на синтаксис, похожий на английский язык (а может именно поэтому), для большинства пользователей VB, которых я вижу на форумах, английский язык не основной. За время работы в Microsoft я несколько раз встречал японцев VB MVP, по крайней мере, один из них постоянно приносил японские конфеты. Если вы VB-программист из Китая, Японии или Кореи (или из любой другой страны, которая использует полноширинные символы), пожалуйста, напишите в комментах. (В комментах автору написали, что японцы стараются везде в коде использовать ascii — Прим. пер.)

Забавный момент: Когда я первоначально реализовал в VB интерполированные строки, я (к своему позору) не учел возможность полноширинных фигурных скобок в местах подстановки. Владимир Решетников (@vreshetnikov) обнаружил и исправил эту ошибку, так что великая традиция толерантности VB к ширине символов осталась в силе.

2. VB поддерживает «умные кавычки»


Ладно, это, конечно, мелочь, но достойная упоминания. Вы когда-нибудь видели пример кода в текстовом документе вроде такого:

ls8lqckokgku3b0raqiboefn9bc.png

А после копирования примера в ваш код обнаруживали, что ни одна из (подсвеченных) кавычек не работает, потому что Word заменяет все обычные кавычки ASCII на "умные кавычки?

Я — нет. Ладно, у меня такое было, но только когда я копировал примеры на C#. В VB умные кавычки являются допустимыми разделителями для строк (забавно, что русские кавычки «» не работают — Прим. пер.):

l0ykwi3ebeigkgrkkrzq8lfrwhm.png

Они также работают внутри строк, хотя, возможно, и странным образом. Если вы удвоите умные кавычки для экранирования, то все, что вы получите во время выполнения, — это просто обычную («глупую») кавычку. Это может показаться немного странным, но тем не менее весьма практично, поскольку почти нигде больше не допускается наличие в строке умных кавычек. Компилятор НЕ заставляет вас обязательно заканчивать умной кавычкой или использовать правильную, если вы начали с умной, так что можете смешивать как угодно и не заморачиваться. И да, это также работает с символом одинарных кавычек, используемым для комментариев:

4y9j8n7mexmkyvtx_vmj-jesfak.png

Я пытался заставить Пола Вика (@panopticoncntrl) признаться, что он сделал это исключительно потому, что замучился с этой проблемой при работе над спецификацией, но он отрицает вину. В VB6 этого не было, так что кто-то добавил это позже.

3. Константы препроцессинга могут быть любого примитивного типа (включая даты) и могут содержать любое константное значение

sd0fjva0vsarlwmx7vgwn8yf8xq.png


4. В выражениях препроцессинга могут использоваться произвольные арифметические операторы

hxcw0k25imfieuvg_ncshwbizim.png

Объявления и пр.


5. VB иногда пропускает объявления implements в IL для предотвращения случайной неявной реализации интерфейса по имени.


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

Interface IFoo
    Sub Bar()
    Sub Baz()
End Interface
 
Class Foo
    Implements IFoo

    Private Sub Bar() Implements IFoo.Bar
        Exit Sub
    End Sub
 
    Private Sub IFoo_Baz() Implements IFoo.Baz 
        Exit Sub 
    End Sub
End Class
 
Class FooDerived
    Inherits Foo 
    Implements IFoo
 
    Public Sub Bar() Implements IFoo.Bar
        Exit Sub
    End Sub
 
    Public Sub Baz()
        ' Does something unrelated to what an IFoo.Baz would do.
    End Sub
End Class


gist.github.com/AnthonyDGreen/39634fd98a0cacc093719ab62d7ab1e6#file-partial-re-implementation-vb

В этом примере класс FooDerived всего лишь хочет переназначить IFoo.Bar на новый метод, но остальные реализации оставить без изменений. Оказывается, что если компилятор просто сгенерирует директиву implements для FooDerived, CLR также подхватит FooDerived.Baz как новую реализацию IFoo.Baz (хотя в этом примере он не связан с IFoo). В C# это происходит неявно (и я не уверен, можно ли от этого отказаться), но в VB компилятор фактически опускает 'Implements' из всего объявления, чтобы этого избежать, и переопределяет только конкретные члены, которые были реализованы заново. Другими словами, если вы спросите FooDerived, реализует ли он IFoo напрямую, он скажет «нет»:

kzu7fr0j1zz4eadll-m5gto7f8q.png


Почему я это знаю и почему это важно? В течение многих лет пользователи VB просили поддержку для неявной реализации интерфейса (без явного указания Implements в каждом объявлении), как правило, для кодогенерации. Просто включить это с текущим синтаксисом было бы breaking change, потому что FooDerived.Baz теперь неявно реализует IFoo.Baz, хотя раньше этого не делал. Но совсем недавно я узнал об этом поведении получше при обсуждении потенциальных проблем в дизайне фичи «реализация интерфейса по умолчанию», которая позволила бы интерфейсам включать реализации по умолчанию некоторых членов и не требовать повторной реализации в каждом классе. Это было бы полезно для перегрузок, например, когда реализация с большой вероятностью будет одинаковой для всех реализующих (делегирование основной перегрузке). Другой сценарий — версионирование. Если интерфейс может включать реализации по умолчанию, вы можете добавлять в него новые члены, не ломая старые реализации. Но тут возникает проблема. Поскольку поведение по умолчанию в CLR заключается в поиске общедоступных реализаций по имени и сигнатуре, если класс VB не реализует члены интерфейса с реализациями по умолчанию, но имеет публичные члены с подходящим именем и сигнатурой, они неявно реализуют эти члены интерфейса, даже если делать это совершенно не предполагалось. Есть вещи, которые можно сделать, чтобы это обойти, когда полный набор членов интерфейса известен во время компиляции. Но в случае, если член был добавлен после компиляции кода, он просто молча поменяет поведение во время выполнения.

6. VB по умолчанию скрывает члены базового класса по имени (Shadows), а не по имени и сигнатуре (Overloads)


Я думаю, это различие довольно хорошо известно. Сценарий таков: вы наследуете базовый класс (DomainObject), возможно, вне вашего контроля, и объявляете метод с именем, которое имеет смысл в контексте вашего класса, например, Print:

Class DomainObject
End Class

Class Invoice
    Inherits DomainObject

    Public Sub Print(copies As Integer)
        ' Sends contents of invoice to default printer.
    End Sub
End Class


gist.github.com/AnthonyDGreen/863cfd1e7536fe8bda7cd145795eaf9f#file-shadows-example-vb

То, что инвойс может быть напечатан, имеет смысл. Но в следующей версии API, где объявлен ваш базовый класс, решают для отладки добавить всем DomainObject’ам метод, который выводит полное содержимое объекта в окно отладки. Этот метод блестяще назвали Print. Проблема в том, что клиент вашего API может заметить, что объект Invoice имеет методы Print() и Print(Integer), и подумать, что это связанные перегрузки. Может, первый просто печатает одну копию. Но это совсем не то, что вы задумали как автор Invoice. Вы понятия не имели, что появится DomainObject.Print. Так что да, в VB это работает не так. Когда всплывает такая ситуация, появляется предупреждение, но что более важно, поведение по умолчанию в VB — скрывать по имени. То есть пока вы явно не укажете с помощью ключевого слова Overloads, что ваш Print является перегрузкой Print базового класса, член базового класса (и любые его перегрузки) полностью скрыты. Клиентам вашего класса показывается только API, объявленный вами изначально. Так работает по умолчанию, но вы можете сделать это явно через ключевое слово Shadows. C# умеет только Overloads (хотя учитывает Shadows, когда ссылается на VB-шную библиотеку) и делает так по умолчанию (используя ключевое слово new). Но это различие всплывает время от времени, когда в проектах появляются некоторые иерархии наследования, где один класс определен на одном языке, а другой — на другом, и имеются перегруженные методы, но это выходит за рамки текущего пункта списка различий.

7. VB11 и ниже являются более строгими к Protected-членам в дженериках


На самом деле мы поменяли это между VS2013 и VS2015. В частности, мы решили не заморачиваться с повторной реализацией. Но я пишу это различие на случай, если вы используете старую версию и заметили его. Вкратце: если в дженерик-типе объявлен Protected-член, то наследник, будучи также дженериком, может получить доступ к этому protected-члену только через наследный экземпляр с такими же аргументами типа.

Class Base(Of T)
    Protected x As T
End Class

Class Derived(Of T)
    Inherits Base(Of T)

    Public Sub F(y As Derived(Of String))
        ' Error: Derived(Of T) cannot access Derived(Of String)'s 
        '     protected members
        y.x = "a"
    End Sub
End Class


gist.github.com/AnthonyDGreen/ce12ac986219eb51d6c85fa02c339a2f#file-protected-in-generics-vb

8. Синтаксис «именованный аргумент (Named argument)» в атрибутах всегда инициализирует свойства/поля


VB использует тот же синтаксис := для инициализации свойств/полей атрибута, что и для передачи по имени аргументов метода. Следовательно, нет способа передать аргумент конструктору атрибута по имени.

9. Все объявления верхнего уровня (обычно) неявно находятся в корневом пространстве имен проекта


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

0yhy7_k2xkchvxq70v1duka5z4a.png


По умолчанию это просто имя вашего проекта в момент создания. Это НЕ то же самое поле, что «Default namespace» в свойствах проекта C#. Default namespace просто задает, какой код добавляется по умолчанию в новые файлы в C#. Но root namespace в VB означает, что, если не указано иное, каждое объявление верхнего уровня в этом проекте неявно находится в этом пространстве имен. Вот почему шаблоны документов VB обычно не содержат никаких объявлений пространств имен. Более того, если вы добавите объявление пространства имен, оно не переопределяет корневое, а добавляется к нему:

Namespace Controllers
    ' Child namespace.
End Namespace

Namespace Global.Controllers
    ' Top-level namespace
End Namespace


gist.github.com/AnthonyDGreen/fd1e5e3a58aee862a5082e1d2b078084#file-root-namespace-vb

Таким образом, пространство имен Controllers фактически объявляет пространство имен VBExamples.Controllers, если вы не избавитесь от этого механизма, явным образом объявив в пространстве имен Global.

Это удобно, потому что экономит один уровень отступов и одно лишнее понятие на каждый VB-файл. И это особенно полезно, если вы создаете UWP-приложение (потому что в UWP все должно быть в пространстве имен), и крайне удобно, если вы решите изменить пространство имен верхнего уровня для всего вашего проекта, скажем, с некоторого кодового имени типа Roslyn на более длинное релизное вроде Microsoft.CodeAnalysis, так как вам не придется вручную обновлять каждый файл в солюшене. Также важно помнить это при работе с кодогенераторами, пространствами имен XAML и новым форматом файла .vbproj.

10. Модули не генерируются как запечатанные (sealed) абстрактные классы в IL, поэтому они не похожи в точности на статические классы C# и наоборот.


Модули в VB существовали до статических классов C#, хотя мы попытались в 2010 году сделать их одинаковыми с точки зрения IL. К сожалению, это было breaking change, потому что XML Serializer (а может это был binary) для той версии .NET (думаю, они исправили это) не хотел делать сериализацию типа, вложенного в тип, который не может быть создан (а абстрактный класс не может). Он бросал исключение.

Мы обнаружили это после внесения изменений и откатили их, потому что некий код где-то использовал тип enum, который был вложен в модуль. А поскольку вам неизвестно, с какой версией сериализатора будет работать скомпилированная программа, это не получится поменять никогда, поскольку в одной версии приложения оно будет работать, а в других — бросать исключения.

11. Вам не нужен явный метод для точки входа (Sub Main) в приложениях WinForms


Если ваш проект использует Form в качестве стартового объекта и не использует «Application Framework» (подробнее об этом в следующем посте), VB генерирует Sub Main, который создает вашу стартовую форму и передает ее в Application.Run, экономя вам таким образом либо целый файл для управления этим процессом, либо дополнительный метод в вашем Form, либо вообще необходимость задумываться об этой проблеме.

12. Если вы вызываете некоторые устаревшие методы рантайма VB (например, FileOpen), вызывающий метод будет неявно помечен атрибутом, чтобы отключить инлайнинг из соображений корректности


Если кратко, методы для работы с файлами в стиле VB6 вроде FileOpen полагаются на контекст, специфичный для сборки, где находится код. Например, файл #1 может быть логом в одном проекте и конфигом в другом. Чтобы определить, какая сборка запущена, вызывается Assembly.GetCallingAssembly(). Но если JIT встраивает (inlines) ваш метод в вызывающий, то с точки зрения стека метод VB-рантайма будет вызван не вашим методом, а вызывающим, который может быть в другой сборке, что затем может позволить вашему коду получить доступ или нарушить внутреннее состояние вызывающего объекта. Это не вопрос безопасности, потому что, если компрометирующий код запущен в вашем процессе, вы уже проиграли. Это вопрос корректности. Поэтому, если вы используете эти методы, компилятор отключает инлайнинг.

Это изменение было сделано в последний момент в 2010 году, потому что x64 JIT ОЧЕНЬ агрессивен при инлайнинге/оптимизации кода, и мы обнаружили это очень поздно, и это был самый безопасный вариант.

13. Если ваш тип помечен атрибутом DesignerGenerated и не содержит каких-либо явных объявлений конструктора, то генерируемый компилятором по умолчанию вызовет InitializeComponent, если он определен для этого типа


В эпоху до появления Partial-типов команда VB вела войну за уменьшение бойлерплейт-кода в WinForms-проектах. Но даже при наличии Partial это полезно, потому что позволяет сгенерированному файлу полностью опустить конструктор, а пользователь может вручную объявить его в своем файле, если он нужен, или не объявлять, если нет. Без этого дизайнер был бы вынужден добавлять конструктор только чтобы вызывать InitializeComponent, а если пользователь добавит тоже, то они будут дубликатами, либо инструментарий должен быть достаточно умным, чтобы переместить конструктор из файла дизайнера в пользовательский и не генерировать его повторно в дизайнере, если он уже существует в пользовательском файле.

14. Отсутствие модификатора Partial НЕ означает, что тип не является partial


Технически в VB только один класс должен быть отмечен как Partial. Это обычно (в GUI-проектах) сгенерированный файл.

Почему? Это сохраняет пользовательский файл красивым и чистым, и может быть очень удобным для включения после генерации или дополнения сгенерированного кода пользовательским. Однако рекомендуется, чтобы максимум один класс не имел модификатора Partial, в противном случае выдается предупреждение.

15. В классах по умолчанию уровень доступа Public для всего, кроме полей, а в структурах Public и для полей тоже


У меня cмешанные чувства по этому поводу. В C# все по умолчанию private (ура, инкапсуляция!), но есть довод сделать в зависимости от того, что вы чаще объявляете: публичный контракт или детали реализации. Свойства и события, как правило, предназначены для внешнего (public) использования, и операторы не могут быть доступны иначе, как public. Однако я редко полагаюсь на доступность по умолчанию (за исключением демо наподобие примеров из этой статьи).

16. VB инициализирует поля ПОСЛЕ вызова базового конструктора, тогда как C# инициализирует их ДО вызова базового конструктора


Слышали, как «некоторые» говорят, что первое, что происходит в конструкторе, — это вызов конструктора базового класса? Ну, это не так, по крайней мере, в C#. В C# перед вызовом base(), явном или неявном, сначала выполняются инициализаторы полей, затем вызов конструктора, а затем ваш код. У этого решения есть последствия, и я думаю, что знаю, почему разработчики языка могли бы пойти тем или иным путем. Я считаю одно из таких последствий — что следующий код не может быть переведен в C# напрямую:

Imports System.Reflection

Class ReflectionFoo

    Private StringType As Type = GetType(String)
    Private StringLengthProperty As PropertyInfo = StringType.GetProperty("Length")
    Private StringGetEnumeratorMethod As MethodInfo = StringType.GetMethod("GetEnumerator")
    Private StringEnumeratorType As Type = StringGetEnumeratorMethod.ReturnType

    Sub New()
        Console.WriteLine(StringType)
    End Sub
End Class


gist.github.com/AnthonyDGreen/37d01c8e7f085e06172bfaf6a1e567d4#file-field-init-me-reference-vb

Во времена, когда я занимался Reflection, я часто писал такой код. И я смутно припоминаю коллегу до Microsoft (Джош), который переводил мой код на C#, иногда жалуясь на необходимость переносить все мои инициализаторы в конструктор. В C# запрещено ссылаться на создаваемый объект до того, как будет вызван base(). И поскольку инициализаторы полей выполняются до указанного вызова, они также не могут ссылаться на другие поля или любые члены экземпляра объекта. Так что этот пример тоже работает только в VB:

MustInherit Class Base

    ' OOP OP?
    Private Cached As Object = DerivedFactory()

    Protected MustOverride Function DerivedFactory() As Object

End Class

Class Derived
    Inherits Base

    Protected Overrides Function DerivedFactory() As Object
        Return New Object()
    End Function
End Class


gist.github.com/AnthonyDGreen/fe5ca89e5a98efee97ffee93aa684e50#file-base-derived-init-vb

Здесь у нас есть базовый класс, который предположительно имеет множество функциональных возможностей, но ему нужен некий ключевой объект для управления, работы, который определяется производным типом. Есть много способов реализовать такой шаблон, но я обычно использую этот, потому что:

  • он краткий;
  • не требует от меня объявления конструктора;
  • не требует, чтобы я поместил код инициализации в конструктор, если он есть;
  • позволяет мне кэшировать созданный объект и не требует, чтобы производные типы объявляли и управляли хранилищем для предоставленного объекта, хотя теперь с автосвойствами это не такая проблема.


Далее, я бывал в обеих ситуациях: когда поле в производном типе хотело вызвать метод, объявленный в базовом классе, и когда инициализатору поля базового класса необходимо было вызвать MustOverride-член, реализованный производным типом. Оба допустимы в VB и ни один в C#, и в этом есть смысл. Если бы инициализатор поля C# мог вызвать член базового класса, этот член мог бы зависеть от полей, инициализированных в базовом конструкторе (который еще не запущен) и результаты почти наверняка были бы неправильными, и это никак не обойти.

Но в VB базовый конструктор уже заведомо отработал, так что вы можете делать что угодно! В обратной ситуации все немного сложнее, потому что вызов Overridable-члена из инициализатора (или конструктора) базового класса может привести к доступу к полям до того, как они будут «инициализированы». Но только ваша реализация знает, является ли это проблемой. В моих сценариях такого просто не бывает. Они не зависят от состояния экземпляра, но не могут быть Shared-членами, потому что вы не можете иметь Shared Overridable-член в любом языке по техническим причинам, выходящим за рамки этой статьи. Кроме того, четко определено, что происходит с полями до запуска пользовательских инициализаторов — они инициализируются значениями по умолчанию, как и все переменные в VB. Никаких сюрпризов.

Так почему же? На самом деле я не знаю, были ли мои сценарии тем, что имела в виду изначальная команда VB.NET, когда они это проектировали. Просто в моем случае это реально работает! Я думаю, что на самом деле все гораздо проще: дизайн VB гарантирует, что вы всегда можете написать в инициализаторе поля то, что вы могли бы написать в конструкторе. Мы интуитивно думаем об инициализаторах полей как о более краткой форме присвоений в конструкторе. При таком дизайне они ими и являются по большому счету.

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

17. Неявно объявленное вспомогательное поле (backing field) событий VB имеет другое имя, нежели в C#, и доступно по имени


Это может быть важно в контексте рефлексии и сериализации (которая на самом деле просто еще бОльшая рефлексия). Если взять простое объявление события с именем E, в VB будет объявлено (скрытое в IDE) поле с именем EEvent. В C# поле также будет называться E, и язык имеет специальные правила, когда выражение E относится к событию, а когда к полю.

18. Неявно объявленное вспомогательное поле автосвойств VB имеет обычное имя и доступно по этому имени


Если объявить автосвойство с именем P, то сгенерируется поле с именем _P'. Оно скрыто в IntelliSense, но может быть доступно при необходимости. В C# это поле имеет «искаженное» (mangled) имя, что означает, что это имя не может быть объявлено или использовано непосредственно в C# и обычно содержит специальные символы.

Почему так? Команда VB решила использовать понятное имя, во-первых, поскольку оно в гармонии с решением по вспомогательным полям событий и переменными «WithEvents», и во-вторых, чтобы имя могло остаться прежним, если автосвойство когда-нибудь будет развернуто в обычное свойство, что важно для сохранения обратной совместимости при сериализации.

19. Неявно объявленное вспомогательное поле read-only автосвойств позволяет запись


Некоторые люди хотели бы, чтобы поля также были доступны только для чтения, для …чистоты. Но в VB существует сильная традиция наличия «спасательных люков» к его волшебным фичам. Хотя вспомогательные поля WithEvents-переменных, non-Custom событий и записываемых автосвойств почти никогда не предназначены для прямого доступа, все же есть скрытый способ обойти аксессоры, если этого требует ваша ситуация. Переменные скрыты от IntelliSense, поэтому вам надо будет приложить усилия, но если вам нужна гибкость, она есть. Философская самосогласованность FTW! Кроме того, это дает VB лаконичную фичу, сравнимую с объявлением private set; автосвойства в C#.

Class Alarm

    Private ReadOnly Code As Integer

    ReadOnly Property Status As String = "Disarmed"

    Sub New(code As Integer)
        Me.Code = code
    End Sub

    Sub Arm()
        ' I'm motifying the value of this externally read-only property here.
        _Status = "Armed"
    End Sub

    Function Disarm(code As Integer) As Boolean
        If code = Me.Code Then
            ' And here.
            _Status = "Disarmed"
            Return True
        Else
            Return False
        End If
    End Function
End Class


gist.github.com/AnthonyDGreen/57ce7962700c5498894ad417296f9066#file-read-only-auto-property-backing-field-is-writeable-vb

20. Атрибуты событий иногда применяются к вспомогательному полю событий


В частности, атрибут NonSerialized.

Поскольку в VB не было синтаксиса для объявления расширенного (expanded) Custom-события до 2005 года (?) и нет синтаксиса назначения атрибута для членов типа, было невозможно явно объявить вспомогательное поле для события и, таким образом, применить атрибут NonSerialized. Это то, что вам обязательно нужно сделать, потому что объекты, слушающие ваши события, на самом деле не являются частью «вашего» состояния и не должны быть частью того, что считается вашим «контрактом данных».

Это сильно мешало некоторым людям, желающим сериализовать объекты, потому что сериализатор попытался бы сериализовать вспомогательное поле события и, таким образом, всех слушателей события. Так, если, например, у вас есть класс данных, который к тому же two-way bindable (а значит, объявляет событие PropertyChanged), сериализатор попытается сериализовать любые контролы, связанные с этим объектом, и, конечно, не сможет этого сделать.

И пример этого, близкий и дорогой моему сердцу, можно найти в ранних версиях фреймворка CLSA «Expert Business Objects» Рокки Лотки (Rocky Lhotka), который и

© Habrahabr.ru