[Перевод] Исчерпывающий список различий между VB.NET и C#. Часть 2
В первой части статьи тема превосходства VB.NET над C# по рейтингу TIOBE нашла живой отклик в комментариях. Поэтому по советуAngReload посмотрим на тренды StackOverflow.
C# все еще силен! Революция, о которой так долго говорили в прошлый раз, отменяется! Ура, товарищи! Или нет? Рейтинг TIOBE строится на основе запросов в поисковиках, а рейтинг SO — на основе тегов задаваемых вопросов. Возможно, разработчики VB.NET, в число которых входит множество людей не айти специальностей, просто не знают о существовании StackOverflow? Или попав туда через гугл, а то и Bing, не понимают, как задать вопрос? А может быть им достаточно документации Miscrosoft, а все немногочисленные вопросы уже отвечены.
Так или иначе, доля VB.NET заметна и стабильна, пусть и не на первом месте по объему. И, конечно, такой результат был бы невозможен без сильной команды проектировщиков и разработчиков языка. Ниже — вторая часть перевода статьи участника этой команды, Энтони Грина.
Содержание
Преобразования
Выражения
Преобразования
34. Булевы преобразования
Преобразование Boolean True
в любой знаковый числовой тип выдает -1
, а в любой беззнаковый — максимальное значение для этого типа, тогда как в C# таких преобразований не существует. Однако метод Convert.ToInt32
, например, преобразует True
в 1
, и именно так он чаще всего представлен в IL
. В обратном направлении любое число, отличное от 0
, преобразуется в True
.
Почему? Причина, по которой VB предпочитает использовать от -1
до 1
, заключается в том, что побитовое отрицание 0 (все биты установлены в 0) на любом языке равно -1
(все биты установлены в 1
), поэтому использование этого значения объединяет логические и побитовые операции, такие как And
, Or
и Xor
.
Также поддерживаются преобразования в и из строк «True» и «False» (разумеется, без учета регистра).
35. Преобразования между типами Enum, а также между типами Enum и их базовыми типами полностью неограничены, даже если Option Strict выставлено в On
С философской точки зрения язык относится к Enum
-типам скорее как к набору именованных констант базового целочисленного типа. Место, где это наиболее очевидно, — равенство. Всегда допустимо сравнивать любое целое число со значением перечисления, тогда как в C# это дает ошибку.
Время историй: API Roslyn прошел много внутренних ревизий. Но в каждой из них для каждого языка было выделено перечисление SyntaxKind
, которое говорит вам, какую синтаксическую конструкцию представляет узел (например, IfStatement
, TryCastExpression
). Однажды разработчик использовал API, которое пыталось абстрагироваться от языка и возвращало одно из значений SyntaxKind
, но только как Integer
, и, не получив ошибки при сравнении сырого Integer
и SyntaxKind
, этот разработчик сразу же пришел ко мне в офис и пожаловался: «int — это деталь реализации, меня должны были заставить сделать приведение!».
Спустя годы, во время очередной ревизии API, мы полностью удалили свойства (Property Kind As SyntaxKind
), которые указывали на специфичный для языка тип, и все API начали возвращать Integer
. Весь код C# сломался, а весь код VB продолжил работать как ни в чем не бывало.
Чуть позже мы решили переименовать это свойство в RawKind
и добавить специфичные для языка методы расширения Kind()
. Весь код C# сломался, потому что для вызова методов были необходимы круглые скобки, но так как в VB они не нужны, весь код VB снова продолжил работать как ни в чем не бывало.
36. Проверка переполнения (overflow)/отрицательного переполнения (underflow) для целочисленной арифметики полностью контролируется средой компиляции (настройки проекта), но VB и C # используют разные значения по умолчанию; в VB проверка переполнения по умолчанию включена
Интегральные типы имеют диапазон, поэтому, например, Byte
может представлять значения от 0 до 255. Итак, что происходит, когда вы добавляете Byte
1 к Byte
255? Если проверка overflow/underflow отключена, значение прокручивается в 0. Если тип со знаком, он прокручивается до самого нижнего отрицательного числа (например, -128 для SByte
). Это скорее всего указывает на ошибку в вашей программе. Если проверка overflow/underflow включена, бросается исключение. Чтобы понять, что я имею в виду, взгляните на этот безобидный цикл For
.
Module Program
Sub Main()
For i As Byte = 0 To 255
Console.WriteLine(i)
Next
End Sub
End Module
исходный код на GitHub
По умолчанию в VB этот цикл будет бросать исключение (поскольку последняя итерация цикла переходит за границу Byte
. Но с отключением проверки overflow он зацикливается, потому что после 255 i снова становится 0.
Underflow — это противоположная ситуация, когда вычитание ниже минимального для типа значения приводит к максимальному значению.
Более распространенная ситуация для переполнения — это просто сложение двух чисел. Возьмите числа 130 и 150, оба как Byte
. Если вы их сложите, ответ будет 280, что не вписывается в Byte. Но ваш процессор воспринимает это не так. Вместо этого он сообщает, что ответ 24.
Кстати, это никак не связано с преобразованиями. Сложение двух байтов дает байт; это просто способ работы двоичной математики. Хотя вы также можете получить переполнение, выполнив преобразование, например, при попытке преобразовать Long в Integer. Без проверки переполнения программа просто отсекает лишние биты и запихивает столько, сколько влезает в эту переменную.
В чем разница? Производительность. Проверка CLR на переполнение требует немного больше вычислительного времени по сравнению с вариантом без проверки, как и все прочие проверки безопасности. VB основан на философии, согласно которой продуктивность разработчиков важнее производительности вычислений, поэтому по умолчанию вам включили проверку безопасности. Команда разработчиков C# сегодня может принять другое решение по дефолтным настройкам проекта, но если учесть, что первые разработчики C# получились из разработчиков C/C++, эта группа людей, вероятно, потребовала бы, чтобы код не делал ничего лишнего, что могло бы стоить циклов процессора; это непростое философское различие.
Нюанс: даже если проверка overflow/underflow выключена, преобразование значений PositiveInfinity
, NegativeInfinity
, NaN
типов Single
или Double
в Decimal
выбросит исключение, поскольку ни одно из этих значений не может быть в принципе представлено в Decimal.
37. Преобразование чисел с плавающей запятой в целочисленные типы использует банковское округление (bankers rounding), а не усечение (truncating)
Eсли вы в VB преобразуете число 1.7 в целое число, результат будет 2. В C# результат будет 1. Я не могу ничего сказать про математические правила за пределами Америки, но я при переходе от действительного числа к целому инстинктивно округляю. И никто из тех, кого я знаю вне круга программистов, не считает, что ближайшим целым числом к 1.7 является 1.
На самом деле есть несколько способов округления, и тип округления, используемый в VB (и в методе Math.Round) по умолчанию называется банковским округлением или округлением статистиков. Его суть в том, что для числа посередине между двумя целыми числами VB округляет до ближайшего четного числа. Так 1,5 округляется до 2, а 4,5 округляется до 4. Что на самом деле работает не так, как нас учили в школе — меня учили округлять вверх от 0,5 (технически, округлять в сторону от нуля). Но, как следует из названия, банковское округление имеет преимущество в том, что при большом количестве вычислений вы делите при округлении пополам, а не всегда раздаете или всегда удерживаете деньги. Другими словами, на большом множестве это ограничивает искажение данных предельным статистическим отклонением.
Откуда различие? Округление интуитивнее и практичней, усечение быстрее. Если вы рассмотрите использовании VB в LOB-приложениях и особенно в таких приложениях, как макросы Excel, работающие на VBA, то простое отбрасывание цифр после запятой может вызвать… проблемы.
Я думаю, очевидно, что способ преобразования — это всегда вопрос неоднозначный и должен указываться явно, но вот если вам нужно выбрать единый…
38. Не является ошибкой преобразовывать NotInheritable классы в/из интерфейсов, которые они не реализуют на этапе компиляции
Вообще говоря, если вы проверяете NonInheritable-класс на реализацию интерфейса, вы можете понять во время компиляции, возможно ли такое преобразование, потому что вы знаете все интерфейсы этого типа и все его базовые типы. Если тип наследуемый, вы не можете быть уверенными, что такое преобразование невозможно, потому что тип объекта времени выполнения, на который указывает ссылка, может по факту иметь более производный тип, который реализует этот интерфейс. Однако есть исключение из-за COM interop, когда во время компиляции может быть не видно, что тип имеет какое-либо отношение к интерфейсу, но во время выполнения это будет так. По этой причине компилятор VB в таких случаях выдает предупреждение.
Почему? VB и COM росли вместе во времена, когда они были детьми в старом районе. Так что в дизайне языка есть несколько решений, в которых VB уделяет большое внимания вещам, которые существовали только в COM на момент релиза .NET 1.0.
39. Попытка распаковать (unbox) null в значимый тип приводит к значению типа по умолчанию, а не к NullReferenceException
Я полагаю, что технически это также верно для ссылочных типов, но да:
CInt(CObj(Nothing)) = 0
Почему? Потому что CInt(Nothing) = 0
, и язык стремится быть в какой-то степени последовательным независимо от того, типизировали ли вы свои переменные или нет. Это относится к любой структуре, а не только к встроенным значимым типам. См. обоснование в #25 для более подробной информации.
40. Распаковка (unboxing) поддерживает преобразования примитивных типов
И в VB, и в C# вы можете конвертировать Short
в Integer
, но что если вы попытаетесь конвертировать упакованный Short
в Integer
? В VB Short
будет сначала распакован, а затем преобразован в Integer
. В C# если вы вручную не распакуете short перед преобразованием в int
, будет брошено InvalidCastException
.
Это относится ко всем внутренним преобразованиям, то есть упакованным числовым типам, преобразованиям между строками и числовыми типами, строками и датами (да, Decimal и Date — примитивные типы).
Почему? Опять же, чтобы обеспечить согласованное поведение, полностью ли строго типизирована ваша программа, типизирована как Object или находится в процессе рефакторинга от одного варианта к другому. Смотрите #39 выше.
41. Есть преобразования между String
и Char
String
преобразуется вChar
, представляющий ее первый символ.Char
преобразуется вString
единственным разумным способом.
Потому что никто, кроме меня, не помнит синтаксис символьного литерала в VB (да и не должен).
42. Есть преобразования между String
и массивом Char
String
преобразуется в массивChar
, состоящий из всех ее символов.- Массив
Char
преобразуется вString
, состоящую из всех его элементов.
Для определенности: эти преобразования создают новые объекты, вы не получаете доступ к внутренней структуре String
.
Забавная история: однажды я нашел (или, возможно, об этом сообщили, и я исследовал) breaking change между .NET 3.5 и 4.0, потому что между этими версиями команда .NET добавила модификатор ParamArray
ко второму параметру перегрузки String.Join
, принимающему массив строк. Точные предпосылки потеряны во времени (вероятно, к лучшему), но, как я считаю, причина в том, что с модификатором ParamArray
теперь можно конвертировать в строку массив Char
, и передать ее как отдельный элемент в массив параметров. Веселая тема.
43 и 44. Преобразования из String в числовые типы и типы дат поддерживают синтаксис литералов (как правило)
CInt("&HFF") = 255
CInt("1e6") = 1_000_000
CDate("#12/31/1999#") = #12/31/1999#
Это работает с префиксами основания и делает возможным очень удобный способ преобразования шестнадцатеричного (или восьмеричного) ввода в число: CInt("&H" & input)
. К сожалению, эта симметрия деградирует на момент написания этой статьи, потому что среда выполнения VB не была обновлена для поддержки двоичного префикса &B или разделителя групп цифр 1_000
, но я надеюсь, что это будет исправлено в следующей версии. Научная нотация работает, но без суффиксов типов, а преобразования даты также поддерживают стандартные форматы дат, поэтому формат JSON, используемый в ISO-8601, также работает: CDate("2012-03-19T07: 22Z") = #3/19/2012 02:22:00 AM#
.
Почему? Я не знаю другой причины кроме удобства. Но я бы очень хотел предложить также поддержку других распространенных форматов, которые сегодня практически повсеместны в сети, таких как #FF, U+FF, 0xFF. Я думаю, это могло бы сильно облегчить жизнь в некоторых типах приложений…
45. НЕТ преобразований между Char
и целочисленными типами
ЧТО??!?!
После прочтения обо всех этих дополнительных преобразованиях вы удивлены? В VB запрещаются преобразования между Char
и Integer
:
CInt("A"c)
не компилируется.CChar(1)
не компилируется.
Почему? Неясно, что должно произойти. Обычно VB в таких ситуациях использует прагматичный и/или интуитивный подход, но для выражения CInt("1"с)
я думаю, половина читателей ожидала бы число 1 (значение символа 1), а половина ожидала бы число 49 (код ASCII/UTF для символа 1). Вместо того, чтобы в половине случаев делать неправильный выбор, VB имеет специальные функции для преобразования символов в коды ASCII/Unicode и обратно, AscW
и ChrW
соответственно.
Выражения
46. Nothing <> null
Литерал Nothing
в VB не означает null. Он означает «значение по умолчанию для типа, в качестве которого оно используется», и просто так сложилось, что для ссылочных типов значением по умолчанию является null. Различие имеет значение только при использовании в контексте, в котором:
- Nothing принимает значимый тип, и…
- Из контекста непонятно, что он это делает.
Давайте рассмотрим несколько примеров, которые иллюстрируют, что это значит.
Первый, возможно немного странный, но я не думаю, что большинству людей взорвет мозг понимание, что эта программа напечатает «True»:
Module Program
Sub Main()
Dim i As Integer = 0
If i = Nothing Then
Console.WriteLine("True")
End If
End Sub
End Module
исходный код на GitHub
Причина достаточно проста: вы сравниваете Integer (0)
со значением по умолчанию его типа (тоже 0). Проблема возникает в VB2005/2008, когда вы добавляете nullable значимые типы. Посмотрите на этот пример:
Module Program
Sub Main()
Dim i = If(False, 1, Nothing)
End Sub
End Module
исходный код на GitHub
Понятно, как кто-то может предположить, что тип i
является Integer?
(Nullable(Of Integer)
). Но это не так, потому что Nothing
получает тип из контекста, а единственный тип в этом контексте исходит от второго операнда, и это простой non-nullable Integer
(технически Nothing
никогда не имеет типа). Другой способ взглянуть на эту проблему — следующий пример:
Module Program
Sub Main()
M(Nothing)
End Sub
Sub M(i As Integer)
Console.WriteLine("Non-nullable")
End Sub
Sub M(i As Integer?)
Console.WriteLine("Nullable")
End Sub
End Module
исходный код на GitHub
Опять же, здесь интуитивно кажется, что Nothing
добавляет подсказку «nullable» и что язык выберет перегрузку, которая принимает nullable
, но он этого не делает (выбирает non-nullable
, поскольку она «наиболее специфична»). Как минимум, можно предположить, что как и null в C#, выражение Nothing
вообще не применимо к Integer
, и что nullable-перегрузка будет выбрана методом исключения, но это опять-таки основано на неправильной мысли, что Nothing = null (Is null?)
.
Нюанс: в C# 7.1 было добавлено новое выражение default
, которое соответствует Nothing
в VB. Если вы перепишете все три примера выше на C#, используя default
вместо null
, вы получите точно такое же поведение.
Что можно сделать по этому поводу? Имеется несколько предложений, но ни одно пока не победило:
- Показывать предупреждение каждый раз, когда
Nothing
преобразуется в значимый тип и это неnull
вnullable
значимом типе. - Красиво разворачивать
Nothing
в0
,0.0
,ChrW(0)
,False
,#1/1/0001 12:00:00 AM#
илиNew T
(значение по умолчанию для любой структуры) каждый раз, когда его значение в рантайме будет одним из перечисленных выше. - Добавить новый синтаксис, означающий «Null, нет, правда!», вроде
Null
илиNothing?
- Добавить новый синтаксис в виде суффикса (?), который оборачивает значение в nullable, чтобы помочь вывести тип, например
If(False, 0?, Nothing)
- Добавить nullable операторы преобразования для встроенных типов, чтобы было легче давать подсказки выводу типа, например,
If (False, CInt? (0), Nothing)
Хотелось бы услышать ваши мысли в комментариях и/или в Твиттере.
Итак, подведем итоги:
- Прежние времена — VB6 и VBA имеют «Nothing», «Null», «Empty» и «Missing», означающие разные вещи.
- 2002 — в VB.NET есть только
Nothing
(значение по умолчанию в конкретном контексте), а в C# — толькоnull
. - 2005 — C# добавляет
default(T)
(значение по умолчанию типаT
), потому что свежедобавленные дженерики создают ситуацию, когда вам нужно инициализировать значение, но вы не знаете, является ли оно ссылочным типом или значимым; VB не делает ничего, потому что этот сценарий уже закрытNothing
. - 2017 — C# добавляет
default
(значение по умолчанию в контексте), поскольку существует множество сценариев, в которых указаниеT
избыточно или невозможно
VB продолжает сопротивляться добавлению выражения Null
(или эквивалентного), потому что:
- Синтаксис будет breaking change.
- Синтаксис не будет breaking change, но в зависимости от контекста будет означать разные вещи.
- Синтаксис будет слишком незаметный (например,
Nothing?
); представьте, что нужно вслух поговорить оNothing
иNothing?
, чтобы что-то объяснить человеку. - Синтаксис может быть слишком уродливым (например,
Nothing?
). - Сценарий выражения значения null уже закрыт
Nothing
, и эта функция будет абсолютно избыточной большую часть времени. - Везде вся документация и все инструкции должны быть обновлены, чтобы рекомендовать использовать новый синтаксис, в основном объявляющий
Nothing
устаревшим для большинства сценариев. Nothing
иNull
по-прежнему будут вести себя одинаково в рантайме в отношении позднего связывания, преобразований и т.д.- Это может быть как пушка в поножовщине.
Как-то так.
Оффтоп (но связанный)
Вот пример, очень похожий на второй выше, но без вывода типа:
Module Program
Sub Main()
Dim i As Integer? = If(False, 1, Nothing)
Console.WriteLine(i)
End Sub
End Module
исходный код на GitHub
Эта программа выводит на экран 0. Он ведет себя точно так же, как и второй пример, по той же причине, но иллюстрирует отдельную, хотя и связанную проблему. Интуитивно понятно, что Dim i as Integer? = If(False, 1, Nothing)
ведет себя так же, как Dim i As Integer? : If False Then i = 1 Else i = Nothing
. В данном случае это не так, потому что условное выражение (If)
не «пропускает» (flow through) информацию конечного типа к своим операндам. Оказывается, это ломает все выражения в VB, которые полагаются на то, что называется конечной (контекстной) типизацией (Nothing
, AddressOf
, массив литералов, лямбда-выражения и интерполированные строки) с проблемами, начиная от некомпилируемости вообще до тихого создания неправильных значений и до громкого выбрасывания исключений. Вот пример некомпилируемого варианта:
Module Program
Sub Main()
Dim i As Integer? = If(False, 1, Nothing)
Console.WriteLine(i)
Dim operation As Func(Of Integer, Integer, Integer) =
If(True,
AddressOf Add,
AddressOf Subtract)
End Sub
Function Add(left As Integer, right As Integer) As Integer
Return left + right
End Function
Function Subtract(left As Integer, right As Integer) As Integer
Return left - right
End Function
End Module
исходный код на GitHub
Эта программа не будет компилироваться. Вместо этого она сообщает об ошибке в выражении If
, что она не может определить тип выражения, когда явно оба выражения AddressOf
предназначены для получения делегатов Func(Of Integer, Integer, Integer)
.
Здесь важно иметь в виду, что решение проблем с Nothing
не всегда означающим null (контринтуитивно), Nothing
не указывающим на nullability
(контринтуитивно) и If(,,)
не обеспечивающим контекст для интуитивного поведения Nothing
(и других выражений) (контринтуитивно) — это все отдельные проблемы, и решение одной НЕ решит другие.
47. Скобки влияют не только на приоритет парсинга; они реклассифицируют переменные в значения
Эта программа выводит на консоль »3»:
Module Program
Sub Main()
Dim i As Integer = 3
M((i))
Console.WriteLine(i)
End Sub
Sub M(ByRef variable As Integer)
variable = -variable
End Sub
End Module
исходный код на GitHub
Аналогичная программа на C# выдаст »-3». Причина в том, что в VB взятие переменной в скобки заставляет ее вести себя как значение — процесс, известный как реклассификация. В этот момент программа ведет себя так, как если бы вы написали M(3)
, а не M(i)
, и никакой ссылки на переменную i
не передается, так что она не может быть изменена. В C# взятие выражения в скобки (по любой причине) не сделает его значением вместо переменной, так что вызов M изменит исходную переменную.
Почему? В VB всегда было такое поведение. На самом деле я только что открыл свою копию Quick Basic (Copyright 1985), и там поведение такое же. С учетом того, что передача по ссылке использовалась по умолчанию до 2002 года, все это очевидно имеет смысл.
Нюанс №1: «Как подпрограмма получила круглые скобки» Пола Вика, заслуженного архитектора Visual Basic .NET.
Нюанс №2: Когда мы проектировали связанное дерево в компиляторах Roslyn (структуру данных, представляющую семантику программы (значение вещей) в отличие от синтаксиса (форма записи вещей)), это стало камнем преткновения для команды компиляторов: будут ли выражения в скобках представлены в связанном дереве. В C# круглые скобки являются почти полностью синтаксической конструкцией, используемой для контроля приоритета синтаксического анализа ((a + b) * c или a + (b * c))
. Настолько, что оригинальный компилятор C#, написанный на C++, отбрасывал факт, что выражение было заключено в скобки, вместе с такими вещами, как пробелы и комментарии. Было несколько попыток согласования между языками: «Можем ли мы посмотреть и избавиться от них в VB?» или «Можем ли мы жить с ними в C#?» и в конечном итоге согласно source.roslyn.io результат — BoundParenthesized
присутствует в компиляторе VB и отсутствует в компиляторе C#. Другими словами, языки здесь различаются, и мы просто должны это принять.
48. Me
всегда классифицируется как значение — даже в структурах
В VB.NET вы не можете присваивать в Me
. Обычно это не вызывает удивления, но кто-то может подумать, что поскольку структуры — это просто набор значений, то допустимо присваивать в Me внутри конструктора или метода экземпляра типа Structure как упрощенную запись копирования. Однако это тоже запрещено и при передаче Me по ссылке будет просто передана копия. В C# допустимо присваивать в this
внутри структуры, и вы можете передать this
по ссылке внутри методов экземпляра структуры.
49. Методы расширения доступны по простому имени
В VB, если для типа определен метод расширения и он находится в области видимости определения этого типа, то внутри определения этого типа вы можете вызвать этот метод по неквалифицированному имени:
Class C
Sub M()
Extension()
End Sub
End Class
Module Program
Sub Main()
End Sub
Sub Extension(c As C)
End Sub
End Module
исходный код на GitHub
В C# методы расширения ищутся только при явном указании получателя (то есть something.Extension). Так что, хотя точный перевод примера выше не будет компилироваться в C#, вы можете получить доступ к расширениям в текущем экземпляре, явно указав this.Extension()
.
Почему? Можно привести аргумент, что к обычным членам экземпляра можно получить доступ без явной их квалификации с помощью 'Me.'
, и, поскольку методы расширения ведут себя везде как члены экземпляра, логично, что они так ведут себя и в этом контексте. VB.NET придерживается этого аргумента. Предположительно есть и другие аргументы, и другие языки вольны их придерживаться.
50. Импорт статиков не объединяет группы методов (Static imports will not merge method groups)
VB всегда поддерживал «статический импорт» (Java-термин, объединяющий модификатор C# с оператором VB). Это то, что позволяет мне написать Imports System.Console
вверху файла и использовать WriteLine()
без квалифицирования в остальной части файла. В 2015 году C# также получил такую возможность. Однако в VB ситуация импорта двух типов с Shared-членами с одинаковыми именами, например System.Console
и System.Diagnostics.Debug
, оба из которых имеют методы WriteLine
, всегда трактуется как неоднозначность. C# объединит группы методов и выполнит разрешение перегрузки, и если есть однозначный результат, то он и будет выбран.
Почему? Я думаю, можно считать, что здесь VB мог бы быть умнее, как C# (особенно учитывая следующее отличие). Но также есть аргумент, что если два метода происходят из двух разных мест и вообще не имеют никакого отношения друг к другу (один не является методом расширения для типа, определяющего другой), то это… вводит в заблуждение, когда все варианты предлагаются под одним и тем же именем.
Более того, в VB есть множество случаев с таким же сценарием, когда VB выбирает более безопасный путь и сообщает о неоднозначности, например, два метода с одинаковым именем из несвязанных интерфейсов, два метода с одинаковым именем из разных модулей, два метода с разных уровней иерархии наследования, где один не является явной перегрузкой другого (отличие #6). VB здесь философски последователен. Кроме того, VB принял все эти решения в 2002 году.
51 и 52. Квалифицирование по неполному имени и умное разрешение имен (Partial-name qualification & Smart-name resolution)
Есть несколько способов трактовать пространства имен:
- С одной стороны, все пространства имен — это соседи в плоском списке и содержат только типы (но не другие пространства имен). Так что
System
иSystem.Windows.Forms
— это соседи, которые имеют по соглашению общий префикс, ноSystem
не содержитSystem.Windows
иSystem.Windows
не содержитSystem.Windows.Forms
. - С другой стороны, пространства имен похожи на папки, организованные в иерархию, и могут содержать другие пространства имен и типы. Так что
System
содержитWindows
, аWindows
содержитForm
.
Первая модель, в частности, полезна для отображения пространств имен в графическом интерфейсе без глубокого вложения. Однако мне всегда была интуитивно ближе вторая. И VB с его директивой Imports следует второй модели, а using
в C# ведет себя согласно первой.
Следовательно, если в VB я импортировал пространство имен System
, я могу получить доступ к любому пространству имен внутри System
, не квалифицируя его с помощью System
. Для меня это все равно, что указать относительный путь. Так что во всех моих примерах, где я квалифицирую ExtensionAttribute
, я пишу
вместо
.
В C# это не так. using System
не добавляет System.Threading
в область видимости под простым именем Threading
.
Но получается даже лучше, потому что C# позволяет сценарий частичной квалификации аналогично относительному пути конкретно в случае, когда код определен в этом пространстве имен. То есть, если вы объявляете тип внутри System
, то внутри этого типа вы можете ссылаться на пространство имен System.Threading
как Threading
. И это последовательное поведение, потому что вы можете объявить пространство имен и тип, лексически содержащиеся в другом пространстве имен, и было бы странно, если поиск по имени внутри типа не нашел бы соседа.
Но получается даже хуже, потому что, хотя и VB, и C# требуют, чтобы пространства имен всегда были полностью квалифицированы в инструкциях Imports
/директивах using
уровня файла, C# позволяет вам иметь директиву using внутри объявления пространства имен, влияя на код внутри этого объявления в этом файле, и в таких директивах using пространства имен могут указываться с использованием простого имени.
Появление квантовых пространств имен (Quantum Namespaces) (неофициальное название)
Но подождите, это еще не все! Модель VB удобна, но это удобство сопряжено с риском. Ведь что происходит, если System
содержит пространство имен ComponentModel
и System.Windows.Forms
содержит пространство имен ComponentModel
? Смысл ComponentModel
становится неоднозначным. И иногда получается, что вы можете просто писать в коде ComponentModel.PropertyChangedEventArgs
, и все будет хорошо (я смутно припоминаю, что более ранние версии дизайнера так делали в сгенерированном коде). Но тогда вы импортируете System.Windows.Forms
(или, может быть, просто добавите ссылку на сборку, которая объявляет подпространство имен с таким именем в том, которое вы импортировали), весь ваш код сломается с ошибками неоднозначности (ambiguity errors).
Поэтому в VB2015 мы добавили интеллектуальное разрешение имен (Smart Name Resolution), при котором если вы импортировали System
и System.Windows.Forms
и пишете ComponentModel
, то по аналогии с котом Шредингера создается квантовая суперпозиция из обеих реальностей, где вы ссылаетесь на System.ComponentModel
и где вы ссылаетесь на System.Windows.Forms.ComponentModel
, пока вы не введете другой идентификатор. Если этот идентификатор представляет собой дочернее пространство имен в обеих реальностях, волна продолжится до тех пор, пока не встретится идентификатор, однозначно указывающий на тип, существующий только в одной временнОй вселенной. В этот момент волна схлопывается, и оказывается, что кот всегда была мертв, т.е. ComponentModel.PropertyChangedEventArgs
должно означатьSystem.ComponentModel.PropertyChangedEventArgs
, потому что System.Windows.Forms.ComponentModel.PropertyChangedEventArgs
не существует. Это позволяет избежать многих неоднозначностей, которые могут возникнуть просто при импорте нового пространства имен.
Но это не решает проблему добавления ссылки, которая приносит новое пространство имен верхнего уровня Windows
в область видимости, потому что пространства имен верхнего уровня (абсолютные пути) всегда побеждают определенные частично (относительные пути) по различным причинам (включая производительность). Поэтому использование WinForms/WPF
и UWP
в одном проекте все еще может быть болезненным.
53. Методы Add инициализатора коллекции могут быть методами расширения
Как упомянуто в #33, когда VB что-то ищет, он обычно рассматривает и методы расширения. Сценарий, когда вам это может понадобиться, — использование краткого синтаксиса инициализатора для коллекций сложных объектов, например:
Class Contact
Property Name As String
Property FavoriteFood As String
End Class
Module Program
Sub Main()
Dim contacts = New List(Of Contact) From {
{"Leo", "Chocolate"},
{"Donnie", "Bananas"},
{"Raph", "The Blood of his Enemies"},
{"Mikey", "Pizza"}
}
End Sub
Sub Add(collection As ICollection(Of Contact), name As String, favoriteFood As String)
collection.Add(New Contact With {.Name = name, .FavoriteFood = favoriteFood})
End Sub
End Module
исходный код на GitHub
Изначально C# не рассматривал методы расширения в этом контексте, но когда мы заново реализовали инициализаторы коллекций в компиляторе Roslyn C#, они стали их учитывать. Это был баг, который мы решили не исправлять (а не фича, которую мы решили добавить), так что это различие актуально только до VS2015.
54. Создание массива использует верхнюю границу, а не размер
На удивление редко упоминается, но при инициализации массива в VB с синтаксисом Dim buffer(expression) As Byte
или Dim buffer = New Byte(expression) {}
размер массива всегда равен expression + 1
.
Это всегда было так в языках Microsoft BASIC, со времен появления инструкции DIM
(означающей dimension — размер). Что, я полагаю, объясняет, почему это так работает: размер массива от 0 до expression. В предыдущих версиях языков Microsoft BASIC можно было изменить нижнюю границу массивов по умолчанию на 1 (и можно было объявить массив с произвольной нижней границей, например 1984), в этом случае верхняя граница совпадала с длиной (я обычно так делал), но эта возможность исчезла в 2002 году.
Но если копнуть глубже, я слышал про модное когда-то веяние в языковом проектировании делать синтаксис объявления моделирующим синтаксис использования, что объясняет, почему массивы в VB объявляются с их верхней границей, почему в BASIC и в C границы массивов указываются на переменной, а не на типе, синтаксис указателей в C, почему типы находятся слева в C-подобных языках. Подумайте об этом, любое использование buffer(10)
будет использовать значение от 0 до 10, а не до 9!
55. Литералы массива VB ср-я магия не то же самое, что неявно типизированные выражения создания массива в C#
Хотя эти две функции часто используются в одних и тех же сценариях, они не одинаковы. Основное отличие состоит в том, что литералы массива VB по своей природе не типизированы (как лямбды) и получают тип из контекста, а в отсутствие контекста — из выражений своих элементов. Спецификация хорошо