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

в 7:44, , рубрики: .net, C#, vb.net, Блог компании JUG.ru Group

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), чтобы сообщить мне ваши любимые отличия и/или те, о которых вы хотели бы узнать поглубже.

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

Содержание

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

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

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

Инструкции

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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 напрямую, он скажет «нет»:

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

Почему я это знаю и почему это важно? В течение многих лет пользователи 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 есть поле:

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

По умолчанию это просто имя вашего проекта в момент создания. Это НЕ то же самое поле, что «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), который использовал бы сериализацию для undo/redo (он сериализовал бы копию того, как объект выглядел раньше, когда вы что-то изменили, и десериализовал, если бы вы отменили изменения), а также клонирования объектов и сетевого маршаллинга. Таким образом, добавление этого особого случая действительно развязало руки затронутым клиентам. Кроме того, весьма изящно, что не нужно полностью заново писать событие вручную, чтобы отказаться от сериализации.

Инструкции

21. Область действия метки — это тело всего метода, ее содержащего; Вы можете прыгать внутрь блоков (не всех)

Так и есть, вы можете снаружи блока выполнить GoTo к метке внутри этого блока. Существуют некоторые ограничения, обычно когда такой переход позволит обойти инициализацию какой-либо важной языковой конструкции. Например, вы не можете перейти в циклы For или For Each; блоки Using, SyncLock или With, и я думаю, что также в некоторых случаях, включающих захват переменных в лямбде и блоки Finally. Но блоки If и Select Case, циклы Do и While, и даже блоки Try — это игра по правилам, и я встречал сценарии с каждым из них:


Module Program
    Sub Main()

        Dim retryCount = 0
        Try
Retry:
            ' IO call.
        Catch ex As IO.IOException When retryCount < 3
            retryCount += 1
            GoTo Retry
        End Try

    End Sub
End Module

gist.github.com/AnthonyDGreen/b93adcf3c3705e4768dcab0b05b187a0#file-try-goto-retry-vb

Причиной этого, скорее всего, является тот факт, что до .NET в VB не было областей видимости типа «блок». В VB6 и раньше вплоть до моего опыта с Quick Basic метки (и переменные) имели область видимости всего содержащего их метода. Когда я начал писать на QB, отступы предлагалось использовать в стилистических целях. Это делало код более читабельным, но это не было отражением структуры «областей видимости», и достаточно часто весь мой код был выровнен по левому краю. К тому же если вы собираетесь использовать GoTo, то вряд ли области видимости блоков для вас — бит старшего порядка, скорее это будет помехой на пути к цели.

Важно: этот сценарий с Try нужно иметь в виду, если VB когда-нибудь получит поддержку await в блоках Catch и Finally, поскольку код, генерируемый при наличии такого GoTo должен быть немного другим.

22. Время жизни локальной переменной <> области видимости

Как продолжение предыдущего пункта, в VB время жизни (как долго эта переменная содержит значение) локальной (не static) переменной не совпадает с ее областью действия (где на нее можно ссылаться по имени). И это имеет смысл, особенно с учетом предыдущего пункта. В моем примере выполнение покинуло бы блок Catch на исключении и повторило попытку до 3 раз. Хотя любые внутренние переменные блока Try находятся вне области видимости блока Catch, и ссылаться на них там нельзя, разумно и необходимо, чтобы при повторном входе в блок Try эти переменные имели прежние значения.

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

Технически, C# определяет, что фактическое время жизни переменной зависит от реализации, поэтому поведение в отладчике не является «неправильным». Просто в VB.NET фактическое время жизни гораздо заметнее.

23. Переменные всегда инициализируются значением по умолчанию для соответствующего типа

Сначала я не собирался об этом говорить, но это часто встречается в обсуждениях дизайна языка, потому что в C# есть такой хардкорный набор правил о «ясном присваивании» (definite assignment). Идея в том, что языку нужны правила, гарантирующие, что вы никогда не получите случайный доступ к «неинициализированной памяти». Это действительно опасно, если остаток (или код) в памяти от некоего предыдущего использования теперь загружен в переменную указателя, которая случайно разыменовывается, и ваше приложение вылетает или система уходит в синий экран. Это часть наследия C/C++. Потому что весь С про производительность, детка! Каждая операция драгоценна, и любое затрачиваемое ЦП время должно быть явным. Таким образом, автоматическое обнуление памяти в целях безопасности до того, как код ее использует, — это правильно. Если пользователь отчаянно хочет гарантированно не получить доступ к мусорным данным, он должен написать это явно, чтобы было ясно, что он просит заплатить за эти циклы ЦП и, в случае, если он все равно уже написал идеально оптимизированный алгоритм, инициализирующий эту переменную неким ненулевым значением, то во всяком случае он не заплатит и за обнуление, и за явную инициализацию. Но да, языки BASIC так не думают, поэтому все наши переменные автоматически инициализируются до значения по умолчанию, и нет никакого доступа к «случайной» памяти, поэтому не требуется для каждой переменной = Nothing, = 0, = False и т.д.

Следовательно, анализ потока (flow analysis) в VB больше похож на рекомендации, чем на реальные правила.

Правила ясного присваивания также облагают большим налогом дизайн языка, потому что правила C# должны быть герметичными, чтобы гарантировать, что вы никогда не получите доступ к переменной в месте, где она не была ясно присвоена по любому пути к этому месту. VB имеет предупреждения в некоторых подобных ситуациях, но изначально они были направлены на то, чтобы помочь разработчикам найти потенциальные источники нулевых ссылок, а не на поощрение более избыточных инициализаторов. В Roslyn, однако, API все еще используют более строгое определение «ясного присваивания», так что ощущения от рефакторинга на высоте, хотя технически переменные всегда ясно присваиваются.

24. RaiseEvent НЕ выдает исключение, если вспомогательное поле равно null

Я видел такое несколько раз, когда кто-то пытался перевести некоторый код C# на VB. RaiseEvent в VB — это не просто перевод прямого вызова вспомогательного поля, он фактически проверяет на null (потокобезопасным способом), поэтому ситуация с null-обработчиком — не то, о чем вам вообще стоит задумываться.

' You don't have to write this:
If PropertyChangedEvent IsNot Nothing Then
    RaiseEvent PropertyChanged(Me, e)
End If

' You don't have to write this:
Dim handlers = PropertyChangedEvent
If handlers IsNot Nothing Then
    handlers(Me, e)
End If

' You don't have to write this either:
PropertyChangedEvent?(Me, e)

' Just write this:
RaiseEvent PropertyChanged(Me, e)

gist.github.com/AnthonyDGreen/c3dea3d91ef4ffc50cfa92c41f967937#file-null-safe-event-raising-vb

Следовательно, хотя использование синтаксиса null-conditional вызова в C# с VS2015 является большим выигрышем для C# в этой ситуации, это гораздо меньший выигрыш для VB (хоть и выигрыш), и я бы никому не советовал заморачиваться, чтобы использовать его без необходимости; идиоматический код VB.NET продолжит хорошо служить вам.

25. Присвоения не всегда одинаковы; иногда присвоение ссылочного типа выполняет поверхностную копию (shallow clone)

Это одно из тех отличий, которые, если вы не заметили их за последние 17 лет, вероятно, не имеют для вас значения. Когда вы присваиваете упакованный (boxed) значимый тип в переменную типа Object, компилятор вставляет вызов метода с именем System.Runtime.CompilerServices.RuntimeHelper.GetObjectValue. Это специальный метод, реализованный внутри CLR. Вот что он делает, принимая ссылку на объект:

  • Если объект является ссылочным типом, он возвращает эту ссылку без изменений.
  • Если объект представляет собой упакованный значимый тип, который является неизменяемым (например, все примитивные типы типа Integer ), он возвращает эту ссылку без изменений.
  • Если объект является любым другим упакованным значимым типом, он копирует значение в новое упакованное значение и возвращает ссылку на него.

Это делается для того, чтобы сохранить семантику значимых типов, которая говорит, что значения всегда копируются, даже в ситуациях с поздним связыванием (late-bound situations). Поэтому, даже если у меня есть упакованная изменяемая структура, и я передаю ее (все еще в упакованном виде) в метод, и этот метод выполняет операции с поздним связыванием над упакованным объектом, которые его изменяют, он все равно работает только с копией значения, а не с копией вызывающего (caller’s copy). Так что будь ваш код слабо типизированным и полностью динамическим, строго типизированным с ранним связыванием, или чем-то средним, — семантика значимых типов остается прежней.

Я столкнулся с этим ровно один раз в своей карьере. Это была программа вроде такой:

Class MyEventArgs
    Property Value As Object
End Class

Structure MyStruct
    Public X, Y As Integer
End Structure

Module Program

    Sub Main()

        Dim defaultValue As Object = New MyStruct With {.X = 3, .Y = 5}
        Dim e = New MyEventArgs With {.Value = defaultValue}

        RaiseEvent DoSomething(Nothing, e)

        If e.Value Is defaultValue Then
            ' No handlers have changed anything.
            Console.WriteLine("Unchanged.")
        End If

    End Sub

    Event DoSomething(sender As Object, e As MyEventArgs)

End Module

gist.github.com/AnthonyDGreen/422ac4574af92d9bbbf59f0fbc40b74d#file-get-object-value-vb

Там было нечто вроде конвейера событий, похожего на конвертер значений WPF, где код начинается со значения по умолчанию и дает другому коду возможность изменить это значение. Если ничего не изменилось, то код пойдет коротким путем. Логично, что если я начал с упакованного значения и аргументы события ссылались на один и тот же упакованный объект после вызова события, то ни один из обработчиков не обновил значение. Но вскоре я понял, что так никогда происходило. Я не думаю, что вообще мог как-то обойти это поведение, поэтому я, вероятно, отказался от использования упакованного значимого типа и заменил свое значение по умолчанию на класс.

Кстати, в документации хелпера указано, что другие «динамические языки» также могут использовать этот хелпер для сохранения семантики значений. Я не проверял IronRuby/Python, но я проверил dynamic в C# (и компилятор C#): C# не добавляет вызовы GetObjectValue при присваивании между динамическими типами. Моим первым инстинктом при проверке этого было использовать object.ReferenceEquals, чтобы понять, были ли ссылки одинаковыми, и это делало копию упакованного значения где-то глубоко в недрах dynamic C# (потому что это был динамический вызов). Но когда я перешел на использование ==, оно не делало копию. Так что C#, по крайней мере иногда, разделяет эту цель сохранения семантики значений в ситуациях позднего связывания.

26. Select Case не поддерживает «проваливание» (fall-through); не требуется break

В приведенном ниже коде Friday является единственным рабочим днем, а Sunday — единственным выходным, остальные 5 дней недели теряются.

Module Program
    Sub Main()

        Select Case Today.DayOfWeek
            Case DayOfWeek.Monday:
            Case DayOfWeek.Tuesday:
            Case DayOfWeek.Wednesday:
            Case DayOfWeek.Thursday:
            Case DayOfWeek.Friday:

                Console.WriteLine("Weekday")

            Case DayOfWeek.Saturday:
            Case DayOfWeek.Sunday:

                Console.WriteLine("Weekend")

        End Select

    End Sub
End Module

gist.github.com/AnthonyDGreen/7b7e136c71dd11b2417a6c7267bb3546#file-select-case-no-fallthrough-vb

Однажды разработчик из команды Roslyn C# перезвонил мне, открыл какой-то код на своем ноутбуке и сказал: «Знаешь, что я сегодня выяснил? Оно не проваливается!» Я отвечаю «Да, не проваливается». Было много смеха. VS фактически удаляет эти двоеточия, если вы их набираете, но так сложилось, что код был сгенерирован, и никто не проверял сгенерированный код, он просто не работал правильно. Но мы его исправили!

Так что это различие по классической причине. C# разработан, чтобы быть знакомым разработчикам из семейства языков C, и именно так работают свитчи в C. Они проваливаются от одного кейса к другому. Между прочим, C# технически тоже не поддерживает проваливание, если только раздел case не полностью пустой. Если вы что-то туда положили, вам нужно или явное goto, или break. В этом контексте в VB существует эквивалент break, Exit Select, но он не нужен в конце блока, потому что в VB нет никакого проваливания.

27. Каждый блок Case имеет свою область видимости

Этот пункт был для меня сюрпризом, когда он проявился в первый раз. Но если вы напишете эквивалентный код для примера ниже в C#, вы получите ошибку:

Module Program

    Sub Main()

        Select Case Today.DayOfWeek
            Case DayOfWeek.Monday,
                 DayOfWeek.Tuesday,
                 DayOfWeek.Wednesday,
                 DayOfWeek.Thursday,
                 DayOfWeek.Friday

                Dim message = "Get to work!"

            Case DayOfWeek.Saturday,
                 DayOfWeek.Sunday

                Dim message = "Funtime!"

        End Select

    End Sub
End Module

gist.github.com/AnthonyDGreen/bd642061896246c9336255881fb78546#file-select-case-scopes-vb

Ошибка будет означать, что message уже объявлена и не может быть объявлена дважды, потому что в C# весь оператор switch представляет собой одну область видимости и каждая метка case — это просто метка. Они не объявляют область видимости. Что, я полагаю, в какой-то степени имеет смысл (по крайней мере, в C): если вы проваливаетесь от одной секции к другой, то, возможно, существует состояние, которое необходимо разделить между секциями.

28, 29, 30. Select Case работает с непримитивными типами, может использовать произвольные неконстантные выражения в проверках и по умолчанию использует оператор =

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

У меня в голове есть отличный пример на эту тему для следующего поста, но сейчас я расскажу о философских причинах, по которым эти вещи отличаются. Это сводится к следующему:

  • Select Case — это сокращение, когда вы хотите протестировать одно и то же значение несколько раз, но…
  • switch — это быстрая инструкция/нативная операция, известная как «таблица ветвлений или переходов».

Это различие, которое объединяет различия 26-30. switch в прошлом ограничивался сценариями, в которых производительность кода, сгенерированного компилятором, выше, чем несколько последовательных проверок if. В IL есть инструкция switch, которая намного эффективнее, чем несколько If, и компилятор VB будет использовать ее в качестве оптимизации при условии, что она быстрее. Но по философии switch в прошлом был ограничен только такими сценариями, полагаю, как наследник веры C в прозрачность производительности. В VB это просто удобство самовыражения.

31. Переменные, объявленные внутри циклов, в некотором роде сохраняют свое значение между итерациями

В этом примере каждую итерацию цикла x имеет то же значение, с которым она завершила предыдущую итерацию, поэтому в консоль выведутся числа -1, -2, -3:

Module Program

    Sub Main()

        For i = 1 To 3
            Dim x As Integer
            x -= 1
            Console.WriteLine(x)
        Next

    End Sub
End Module

gist.github.com/AnthonyDGreen/cbc3a9c70677354973d64f1d993a3c5d#file-loop-variables-retain-their-values-vb

Технически каждая итерация «делает свежую копию всех локальных переменных, объявленных в этом теле, инициализированную предыдущими значениями переменных» (согласно спецификации). Это тонкое различие, которое стало заметным только в VB2008 и дальше, когда были введены лямбда-выражения:

Module Program

    Sub Main()

        Dim lambdas = New List(Of Action)

        For i = 1 To 3
            Dim x As Integer
            x -= 1
            lambdas.Add(Sub() Console.WriteLine(x))
        Next

        For Each lambda In lambdas
            lambda()
        Next

    End Sub
End Module

gist.github.com/AnthonyDGreen/2ef9ba3dfcf9a1abe0e94b0cde12faf1#file-loop-variables-captured-per-iteration-vb

Этот пример также выводит -1, -2, -3. Потому что технически каждая x — «свежая копия», лямбда-выражение захватывает значение x только для этой итерации, что чаще всего соответствует вашим ожиданиям. Но вы все равно должны перенести значение из предыдущих итераций, как если бы это была одна переменная x на все время жизни цикла. Попробуйте посмотреть это во flow analysis API — рискните! («Переменная… присваивается… самой себе?»)

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

Кстати, команды разработчиков VB и C# решили изменить поведение управляющих переменных (control variables) в For Each в VS2012 (?), чтобы лямбда-выражения захватывали их «на итерацию». Это на 10000% более практично и интуитивно, чем было раньше (на самом деле, VB раньше показывал предупреждении в этом сценарии, потому что поведение было слишком неинтуитивно). Кроме того, команда разработчиков языка VB очень серьезно рассматривала изменение поведения управляющих переменных в For, чтобы они вели себя как переменные внутри цикла. То есть чтобы вы все еще могли изменить их значение внутри цикла, но после захвата текущее значение замораживалось. Это изменение рассматривалось вместе с идеей, что в VB циклы For были намного ближе к циклам For Each, чем for к foreach в C#. В конечном счете мы так и не внесли это изменение, но цикл For в VB по-прежнему выдает предупреждение, когда захватывается управляющая переменная, потому что поведение часто становится сюрпризом.

32. Три выражения цикла For вычисляются только один раз в начале цикла

После начала вы не можете изменить границы цикла For. Выражения в заголовке цикла вычисляются только один раз, а результаты кэшируются, поэтому в этом примере будет напечатано 1,3,5,7,9, даже если изменение верхней границы и инкремента заставляет вас думать, что он будет крутиться вечно.

Module Program

    Sub Main()

        Dim lower = 1,
            upper = 9,
            increment = 2

        For i = lower To upper Step increment
            Console.WriteLine(i)
            upper += 1
            increment -= 1
        Next

    End Sub
End Module

gist.github.com/AnthonyDGreen/1e48113be204f515c51e221858666ac7#file-for-loop-bounds-cached-vb

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

Тем не менее, я не уверен, что мир будет работать без этого, учитывая, что, в отличие от цикла в стиле C, условие цикла в VB выводится. Вы когда-нибудь задумывались, как VB понимает, цикл For i = a To b Step c считает вверх (и должен остановиться, когда i> b ) или вниз (и должен остановиться, когда i <b ), особенно если c неизвестна во время компиляции? Это довольно захватывающее чтение, особенно при позднем связывании, но этот карточный домик рухнул бы, если бы b иногда было положительным, а иногда — отрицательным. Я даже не уверен, чего в этом случае ожидать, но, благо, мне никогда не придется об этом думать.

33. For Each циклы в VB могут использовать метод расширения GetEnumerator

Чтобы тип можно было использовать в For Each, типу не нужно реализовывать IEnumerable, компилятору просто должен быть доступен метод GetEnumerator в коллекции, по которой делается For Each.
Например, я всегда считал, что должна быть возможность For Each по IEnumerator в ситуациях, когда вы уже использовали его часть и хотите возобновить итерирование, например:

Module Program

    Sub Main()

        Dim list = New List(Of Integer) From {1, 2, 3, 4, 5}

        Dim info = list.FirstAndRest()

        If info.First IsNot Nothing Then
            Console.Write(info.First.GetValueOrDefault())

            For Each other In info.Additional
                Console.Write(", ")
                Console.Write(other)
            Next

            Console.WriteLine()
        End If
    End Sub

    <Runtime.CompilerServices.Extension>
    Function FirstAndRest(Of T As Structure)(sequence As IEnumerable(Of T)) As (First As T?, Additional As IEnumerator(Of T))
        Dim enumerator = sequence.GetEnumerator()

        If enumerator.MoveNext() Then
            Return (enumerator.Current, enumerator)
        Else
            Return (Nothing, enumerator)
        End If
    End Function

    <Runtime.CompilerServices.Extension>
    Function GetEnumerator(Of T)(enumerator As IEnumerator(Of T)) As IEnumerator(Of T)
        Return enumerator
    End Function
End Module

gist.github.com/AnthonyDGreen/d7dbb7a5b98a940765c4adc33e3eaeee#file-for-each-extension-get-enumerator-vb

В этом примере я взял очередь у моих друзей из F# и разделил последовательность на первый элемент и остаток, а также расширил IEnumerator, чтобы я мог выполнить For Each на всех неиспользованных элементах, оставшихся в последовательности.

В VB есть общая тема, что когда языковой конструкции нужно найти член с хорошо известным именем (well-known name), этот член может быть методом расширения. Это также относится, например, к методу Add, используемому инициализаторами коллекции. C# по умолчанию так не делает, но с каждой версией относится к этому все проще (см. async/await). На самом деле там был баг, в котором компилятор C# Roslyn (случайно) делал это для инициализаторов коллекций, и они решили его оставить.

Минутка рекламы. 15-16 мая в Санкт-Петербурге состоится конференция для .NET-разработчиков DotNext 2019 Piter. Будет множество докладов, касающихся деталей работы и внутреннего устройства платформы. Программа всё ещё находится на этапе формирования, но около половины докладов уже известны. На официальном сайте можно ознакомиться с программой и приобрести билеты.

Автор: Алексей Мерсон

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js