Comments 230
Да фичи то ещё ладно, а вот синтаксис для них они выбирают какой-то слишком странный.
В MiddleName?[0]
знак вопроса ни к селу, ни к городу, семантики никакой. Сделали бы просто, что для переменной Nullable-типа вызов любого метода возвращает null обёрнутый в Nullable-тип возврата, если в самой переменной был null.
^0
как автор в статье написал, лучше бы для возведения в степень использовали.
А тут можно было бы ~0
термин recursive patterns — это еще на порядок хуже
Зачем для этого вообще отдельный термин? Это же самый обычный паттерн-матчинг с нормальной деконструкцией и биндингом сматченных значений в переменные.
Можно было бы обозвать "Pattern matching restrictions have been eased."
`~`
уже есть. Так что использование ~0 создало бы неоднозначность в грамматике — то ли это индексирование с конца, то ли побитное инвертирование, как раньше. Поэтому добавили унарный оператор ^
. Он, кстати, никак не мешает добавить позже бинарный оператор ^
для возведения в степень.Знак вопроса уже используется в подобных местах, скажем
x ?? -1
, возвращает х, если тот не пустой, или -1. Так же логично сделали x?.foo, x?[0]
. Менять поведение по умолчанию и семантику существующего кода не хотят, ввели новые операторы.Ну кстати ~0
для интов как раз отлично бы работал %)
Флип всех битов как раз даёт ~0 == -1
, ~1 == -2
и т.д., можно было бы сделать отрицательные индексы для "с конца", таким образом сохранить соместимость с питонистами, а остальным объяснить что надо ~0
юзать
Про унарный оператор ~
v0s меня уже опередил. С этим как раз ничего менять не пришлось бы. Технически бинарный оператор ^
добавить то можно, но вряд ли одному символу станут придавать настолько разный смысл в зависимости от кол-ва операндов.
А x?.
по мне всё равно дико смотрится, особенно для тех, у кого есть опыт работы с Ruby, где существует совершенно логичная конвенция, что x?
всегда возвращает boolean, ну типа active?
вместо isActive
. Думаю, всё равно в итоге нормальную Maybe монаду завезут и эти? после x станут deprecated.
Вы давно не смотрели в kotlin. Давно же появились платформенные типы — String!
, которые могут использоваться как nullable, как и non-null.
Не, этот тип применяется только когда nullability неизвестна (когда дёргаем java код, на котором нет никаких аннотаций). Тогда компилятор позволит работать с этим типом как с nonnull, но везде натыкает проверок на null для early null propagation. Т.е. когда есть аннотации — то в Kotlin всё будет хорошо и везде будет нужный тип. Если нет (старый код или просто не заморачивались) — то можно не писать портянки из всяких ?., ?:,!!! и прочего и просто писать код, не столь безопасный (хотя если где-то прилетит null, где вы его не ожидали — то вылетит как можно раньше, а не когда этот null расползётся по всем структурам), но без манускриптов из символов и просто красивый.
Ну nullability в kotlin на уровне типов, override не аннотация, а модификатор функции, его отсутствие — ошибка компиляции.
Посмотреть на разные особенности языка можно онлайн, например на https://play.kotlinlang.org/koans
Просто выбираете нужную тему и смотрите, как она реализована в данном случае.
Как и со всеми языками — нужно пробовать, желательно не на хеллоуворлдах.
А вообще он решает много попоболи из Java, простой (относительно какой-нибудь Scala например), фичастый (корутины, инлайновые классы вон завозят потихоньку и т.п.), ну и с недавних пор мультиплатформенный (jvm, js, native).
Не помню кто сказал (кажется это был Андрей Бреслав), когда пишешь на Котлине — прям на душе приятно. Ты не пишешь очередные геттеры, а пишешь код.
Поэтому по мне все эти восторженные возгласы исключительно из-за появления современного и модного языка, который развивается и в котором уже есть почти все в отличии от других более старых ЯП, для которых многие библиотеки уже и вовсе не развиваются. Типа о, он даже лямбды из коробки поддерживает, вот это да! А корутины? Скоро будут? Вот это сервис, прям индивидуальный подход!) Я так это вижу…
P. S. Но по поводу геттеров и сеттеров посмотрю, спасибо, это проблема многих языков на самом деле и несколько изматывает при создании нового класса все это реализовывать вручную, а они это как реализовали?
А тут можно было бы ~0
~0
вернёт обычный int
. Это будет являться проблемой, т.к. усложнит индекацию и поломает существующий код, где индекс не является неотрицательным и начинается с нуля. Должен возвращаться объект типа Index
, чтобы всё работало корректно.
^0 как автор в статье написал, лучше бы для возведения в степень использовали.
Оператор ^ уже используется для операции XOR. Его нельзя использовать для возведения в степень. Так как унарного ^ не было, его решили использовать таким образом — всё в порядке.
А для возведения в степень разумным выглядит такая конструкция: a ** b
.
С опретором **
тоже могут быть проблемы: Proposal: Exponentiation operator.
Зачем для этого вообще отдельный термин?
Как минимум по бюрократическим причинам. В рамках C# 8.0 ведётся работа над некоторой подфичей Pattern Matching. Чтобы при разговорах о ней не размахивать руками в воздухе или не ссылаться на неё по номеру (Feature #5982), необходимо было придумать какое-то человеческое название. Конечному пользователю языка, если он на github.com/dotnet/csharplang не ходит и спецификации не читает, это название ненужно.
Но название фичи ведь должно кратко описывать её суть, а не просто из случайных слов состоять? А тут в чём рекурсивность? Тут скорее "Nested deconstruction"
Можно написать паттерн внутри паттерна внутри паттерна внутри паттерна… Выглядит, правда, адски, как ни форматируй.
Можно назвать nested, можно назвать recursive ― примерно одинаково.
var person = new Person
{
Name = new FullName
{
First = "Vasya",
Last = "Pupkin",
},
Age = 42,
};
if (person is { Age: 42, Name: { First: var firstName, Last: "Pupkin" } })
{
Console.WriteLine(firstName);
}
А тут в чём рекурсивность?
Потому, что на вопрос "из чего может состоять паттерн?" среди можно ответить "паттерн" — т.е. он как бы определен через самого себя.
Рекурсивность появится если паттерн будет состоять из самого себя, а не просто из набора других паттернов.
Если бы было так:
var person = new Person
{
Name = new FullName
{
First = "Vasya",
Last = "Pupkin",
X = "secret",
},
Age = 42,
};
if (person is { X: var x})
{
Console.WriteLine(x);
}
то можно было бы сказать, что есть рекурсивный паттерн, который соответственно запускает рекурсивный поиск совпадения на любую глубину вложенности.
^0 как автор в статье написал, лучше бы для возведения в степень использовали.
А тут можно было бы ~0
странная идея о том, что стандарт языка должен строиться на том, что 5% его пользователей пользуются латехом, и, следовательно, он не должен латеху противоречить и должен быть похож. Особенно когда язык далеко не о математике (по сравнению с другими языками).
Ну и что ~0 что ^0 вообще интуитивно никак не намекают на то, что они делают. Лучше уж какой-то префикс типо reverse и т. д.
Забавно читать ваш комментарий в 2024 году.
docs.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-8.0/ranges
и в описании новых фич:
docs.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-8#indices-and-ranges
The ^0 index is the same as sequence[sequence.Length]. Note that sequence[^0] does throw an exception, just as sequence[sequence.Length] does.
То есть ^0 это не последний элемент, как написано в статье, а индекс за-последним. Индексировать по нему нельзя, выбросится исключение! Это то же самое, что collection::end() в С++.
В Майкрософтовском предложении для взятия последнего элемента есть пример
var lastItem = list[^1]; // list[Index.CreateFromEnd(1)]
Разобравшись с индексами, становится понятно, что никакой путаницы с тем, включается ли последний элемент диапазона, нет — он никогда не включается! Независимо от того, берется ли последний индекс в диапазоне с начала или с конца.
[0..^0] работает и описывает весь массив именно потому что второй индекс ^0 не включается, иначе бы бросалось исключение.
Получается какой-то substr. 0..4 тут 4 элемента от нулевого. Да нет, зреет получается. 4..5 выберет один элемент? Они упрлс?
Вот если бы было как описал автор, то последнее значение в Range не включено, если индекс с начала, то включено, если индекс с конца — я бы решил что что-то не так.
Что нормального в том, что в 4..5 помещается один элемент? Как это читать тогда вообще?
Все нормально и очевидно: 5-4=1.
Но, конечно, Apple не может же использовать опыт того самого Xerox PARC, с которого они слизали интерфейс MacOS (оригинальной версии, не MacOS X).
А так — полузакрытые интервалы и нумерация с нуля — это просто удобно. Может быть не слишком привычно для человека без опыта, но удобно.
интересно в каком языке все таки сделали полное математическое описание, с круглыми и квадратными скобками?
Вот всё как вы хотели: и все четыре варианта предусмотрены и да — таки круглые и квардартные скобки. Но «новое поколение» хочет, видимо, лично понаступать на те же грабли. Языки, созданные до их рождения, видимо «неправильные» — наверное за полвека что-то кардинально новое в математике случилось…
В чём смысл такой записи?Вы в каждой строчке программы хотите увидеть «смысл жизни»? Или как?
С полуинтервалами легко работать: их объединение — это тоже полуинтервал если они пересекаются — и это пересечение легко проверить, их разность — это один-два полуинтервала и там тоже всё легко считается. Самая важный прицип в программировании (разделяй и властвуй) с полуинтервалами делается тривиально, а с интервалами или отрезками — с трудом.
И это всё совершенно не зависит от того — рассматриваете ли вы вещественные числа или целые.
Может быть, описание открытых интервалов само по себе и имеет смысл (хотя лично мне он неведом, это же целые числа в любом случае), но для срезов…Вот именно для срезов полуинтрвалы и лучше. Потому что вы их делаете не для того, чтобы на доску повесить, а для того, чтобы какой-то алгоритм реализовать. И вот там +1/-1 встречается реже, если использовать полуинтервалы.
Как уже упоминалось: попытки использовать другие варианты в истории были. Mesa вообще все четыре варианта поддерживала. Но оказалось, что это — хуже: если у вас в 70% случаев полуинтервалы, а в 30% случаев — что-то другое (да, иногда могут и отрезки и даже интервалы оказаться удобными), то в этих случаях возникает путаница уже из-за того, что человек забывает, что вот конкретно здесь у него не полуинтервал, а что-то другое. Лучше в этих [относительно редких] случаях +1/-1 написать…
Но вы так убедительно говорите, что может быть, я просто привык.
Вы как хотите, но мне это по-прежнему кажется некрасивым. Тогда уж вводить синтаксис для закрытой и открытой границы — причём любой, правой или левой. Тогда было бы удобно — я писал бы что-то вроде [0..limit] и (limit… array.size()).
Если limit
оказался за правым краем (limit == count
), то [0..limit]
вылетит без видимых причин. "Красивый код" обезобразится некрасивый проверкой, которая будет торчать посреди красивого кода камнем посреди пустыни. А код с явным добавлением единицы органично требует такой проверки, явно указывает на её необходимость, так что вы про неё, во-первых, не забудете, а, во-вторых, не будете плеваться, натыкаясь на неё глазами потом.
То есть я ищу номер первого элемента (назовём его limit), при котором сумма станет не меньше установленного значения
Поменяйте условие на "дай номер элемента, следующего за тем, при котором бла-бла-бла, либо длину всей последовательности" — и всё, вам не нужно делать limit+1, потому что limit теперь — это одновременно начало второго и конец первого интервалов:
var sum = 0;
var limit = Math.Min(
seq.Length,
seq.TakeWhile(i => (sum += i) < sumLimit).Count() + 1);
var head = ..limit;
var tail = limit..;
мне это по-прежнему кажется некрасивым
Некрасиво это лишь потому, что вы ищете последний элемент интервала, а не первый элемент следующего (который одновременно является концом предыдущего интервала). Просто нужно приучить себя думать в терминах начал последовательностей и их длин, и писать алгоритмы используя только их. Примерно как в случае с числами с плавающей точкой всегда думаем в терминах строгих неравенств "<" или ">", и почти никогда не используем операторы "==", "<=", ">=", "<>".
Хорошая эвристика для code review, между прочим: если в алгоритме с плавающей точкой встречаешь знак равенства (даже в составе "<="), или в алгоритме с последовательностями встречаешь "последний элемент" или "index + 1", то внутренне напрягись и жди ошибки.
В чём смысл такой записи?
- Попробуйте взять пустой интервал если запись
a..b
эквивалентна отрезку[a,b]
, а не полуинтервалу[a,b)
. Отрезок[a,a]
непустой, он состоит из 1 элементаa
. Писатьa..a-1
, что ли? Илиa+1..a
? Для полуинтервалов же это не проблема,[a,a)
— пустой. - Попробуйте взять два смежных отрезка.
[a,b]
и[b,c]
не подходят, так как элементb
входит в оба отрезка. Писатьa..b-1
иb..c
? Илиa..b
иb+1..c
? Для полуинтервалов же это не проблема,[a,b)
и[b,c)
— смежные и не пересекаются, их суммой самым натуральным образом оказывается[a,c)
.
a..b
выглядит симметричной при том, что a
входит в интервал, а b
— не входит. Что, действительно, выглядит немного странно: конструкция вроде симметрична, а на самом деле — нет.Но это уже чисто вопрос привычки: получинтервалы реально удобнее, а что немного непривычно выглядят — так тут уж «как получилось».
Возможно так. Хотя взять ту же пару std::begin(), std::end()
— вроде выглядит симметрично, на на деле нет. Тех, кто интервалов касался, эта кажущаяся симметрия вряд ли введёт в заблуждение.
Как тут, симметрично?
for (int i = a; i < b; i++)
Так это не просто сферический интервалы в вакууме. Это срезы. Вам часто нужно брать пустой срез?
К интервалам как таковым претензий нет, end() Это действительно удобно.
Вам часто нужно брать пустой срез?
Да, например, при парсинге строки часто вылазят подстроки нулевой длины.
В чём принципиальная разница, не улавливаю? Что срез, что интервал — это просто функция, отражающая пары чисел (границы) на множества подпоследовательностей (интервалы). Если я пишу a..b, то мне нужно, чтобы множество всех возможных подпоследовательностей включало в себя и пустые, для полноты и общности.
Нормальный функциональный код.
Стареете, хуже воспринимаете нововведения — это нормально.
Например Реймонд Ваган Дамадьян, изобретатель МРТ, ему сейчас около 80 лет.И что он такого сделал «прорывного» в 5 лет? Когда он свою первую статью про МРТ публиковал — ему 35 было. А IT… В основном сейчас на Big Data молются и прочие всякие Map Reduce. Джеффу Дину, когда он это всё придумал — было 32. И это — в общем типичная история: случаи, когда «седовласые дядьки за 50» что-то реально изобретают (а не приписывают себе что-то, что сделали другие) — случаются, но их очень-очень мало.
Именно они стоят у руля лабораторий, которые дают вам технологие, на которые вы молитесь.Стоять у руля != изобретать. Многие изобретения вообще случаются потому, что учёные, которые их делают всё проводят «мимо» руководителей.
И это конечно факт прискорбный, что «истина недоступна большинству», но ожидаемо — так что по барабану.Как раз большинству она доступна. А вам — похоже, что нет.
А сколько Торвальдсу когда линукс стал мейнстримом и реально вошел в жизнь каждого?А причём тут это вообще? Торвальдс уже давно код сам не пишет и, фактически, играет роль того самого «заведующего лабораторией». Под руководством которого другие люди занимаются инновациями. Последняя действительно прорывная вещь, которую он сделал — это был Git. Что случилось больше 10 лет назад и было ему тогда, оп-па, таки 36.
И я привел парочку примеров в подтверждение (помните чего? именно неверности «Стареете, хуже воспринимаете нововведения»).Это где, извините? Всё что вы привели — несколько примеров того, как человек в 20-30-40 лет что-то сделал — что захватило мир гораздо позже. И те же самые люди, которые в молодости «переворачивали мир» — в старости начинают, зачастую, выглядеть чудаками. Как тот же Столлман, который реально очень сильно изменил путь всей индустрии когда-то… а сегодня отказывается пользоваться браузером, чем всех умиляет.
Потому что он утверждает, что знает, что именно определяет мое мнение.Нет, он просто шутит. А вот уже то, что вы безобидные шутки воспринимаете «в штыки» — заставляет хлопнуть себя ладонью по лбу и громко расхохотаться.
И Аристотель, и многие ученые мужи современности, и собственно лично я, считаем именно так. Вы конечно можете думать как вам вздумается. Но на этой двойственности мнений мы и закончим с этим пунктом.Ну, Аристотель — он, конечно, дядька умный, да. Но вот вы ответьте на один вопрос: если вам нужно будет количество лап у мухи узнать — вы просто возьмёте и посчитаете или будете тоже схоластику разводить?
Но это же не означает, что подобный код перестает быть Г.
Нет, не означает, но он и в самом деле перестаёт быть этим самым Г.
Я называю это "нулевой рефакторинг": нулевой рефакторинг — это частный случай рефакторинга кода, при котором код — как текст — не изменяется, но при этом (иногда кардинально) меняется его архитектура, архитектура программы. Нулевой рефакторинг может быть итоговым результатом настоящего сложного многостадийного рефакторинга когда в итоге неожиданно оказывается, что получился "полный круг", а может совершаться исключительно в голове программиста в результате повышения его кругозора и опыта работы.
Вот чтобы обсуждаемый код перестал быть "Г.", требуется именно нулевой рефакторинг, и большое количество людей в отрасли его уже проделали.
А как вы определили, что он "абсолютно не читаем", а не просто "непривычен"?
— этот код легко читаетсяС чего вы взяли? Кто вам сказал что он легко понимается всей командой разработчиков?
Так какая выгода от такого матчинга? Один вред.Реализация функционального подхода наверное.
— не нужно набирать тонну говноскобокНужно набирать тонну говно-if'ов
— есть возможность использовать разные типы исключенийКто вам запрещает вместо строки возвращать разные исключения?
легко проверяется во время дебага (даже точки останова ставятся элементарно)Unit test, не слышали?
Внезапно у вас поменялась структура объекта, который надо валидировать, и вы радостно переписываете все свои if'ы.
но обсуждаем то, что в статье,
Да и что вы будете делать, если...Как мне кажется это взаимно исключающие вещи.
если вам так будет легче, то в рассматрвиаемом примере кода, реализация функционального подхода — вред для читаемости и сопровождения.Не вы вызывает затруднений у меня. И как бы это же синтетический пример, вы помните?
А что такое читаемость я сейчас расскажу:Серьезно?
В своей работе, разработка приборов учета и ПП безопасности, несколько лет успешно используем ДРАКОН. Для нас код вторичен.
там словесный понос.В вашей комнате разработчиков это допустимая словесная форма?
этот код легко читается
Почему вы считаете что он читатся легко? Рациональные аргументы?
if (person.PhoneNumber == null)
throw new ArgumentException("Phone number missing entirely");
if (person.PhoneNumber.Number == 0)
throw new ArgumentException("Actual number missing");
if (person.PhoneNumber.Code < 0)
throw new ArgumentException("WTF?");
Лично мне не очень нравится три раза писать и читать одно и то же.
— не нужно набирать тонну говноскобок
Круглые не считаются?
модификация кода валидации на что-то более сложное не требует изменений всего куска кода
Это также аргумент чтобы заменить a * 2 на switch(a){ case 1: return 2; case 2: return 4
Расскажу как для меня видятся эти два кусочка кода, возможно проследив за моим ходом мыслей вы поймете, что бывают люди, думающие по другому, и это тоже правильно (а может быть и более правильно):
Код с матчингом читается легче, потому что все ветки находятся на соседних строчках, друг под другом, и их легко визуально сравнивать. Тот самый ваш пример про два хеша — тут то же самое. Код с if — разделен блоками кода — это в некоторой степени засоряет визуальное восприятие. Однако главное не это.
Код с шаблонов описывает сам себя — он декларативен, он как две картинки с отличиями, которые дети сравнивают в детском саду. Это просто как дышать. Код же с if — императивен. Мне надо загрузить строчку сравнения в "оперативную память", чтобы понять что именно там сравнивается. А чтобы сравнить два условия — мне вообще необходимо нагрузить свой мозг. Сравните это с "в верхней строчке после двоеточия null, а в нижних — что-то… все понятно".
При дебаге кода с сопоставлением шаблона, я ставлю одну точку останова — в месте инициализации person, затем вывожу его визуальное представление и визуально сравниваю с шаблоном. Один раз делаю операцию из детского сада. Я точно уверен, что внутри кейса person никак не изменится — мне вообще не нужно жать step forward, потому что это всего один шаг.
Сравните с кодом с if, где в принципе объект person может поменяться по дороге (по крайней мере теоретически). Кроме того, я скорее всего буду жать step forward 4 раза, и сравнивать конкретные вещи 4 раза — в первом случае это person с null, потом PhoneNumber с null и так далее. По-моему, когнитивная нагрузка и временные затраты в этом случае больше.
Набирать много скобок или много if — это в принципе дело вкуса, и врядли аргумент. Если бы это было важно для вас, вы бы программировали на Питоне. А тут всетки не Лисп, чтобы не это ругаться. Выглядит как обычный json.
Очевидно, что вместо строк объекту person можно было при присвоить эти самые разные исключения, чтобы потом их зарейзить:
{ PhoneNumber: null } => new ArgumentException("Phone number missing entirely"), ... throw error;
Вот только в этом коде и Вашем есть большая разница: Ваш код нарушает базовые принципы структурного программирования. Наличие более одной точки выхода из блока кода значительно увеличивает когнитивную нагрузку (надо держать в голове где там оно могло прервать поток исполнения) и приводит к багам при дальнейшем изменении этого участка кода. 4 throw это 4 goto по своей сути. Возможно, программисты C# привыкли к такому, и это наоборот best-practise, но я предпочитаю в этом вопросе следовать Дейкстре неукоснительно.
По-моему все ровно наоборот. Предлагаю вам не изменяя целиком ваш блок, добится того же, что и я, при добавлении одной строки:
var error = person switch { null => "Object missing", { PhoneNumber: null } => "Phone number missing entirely", + { PhoneNumber: { Number: 0, Code: var code} } when code < 0 => null, { PhoneNumber: { Number: 0 } } => "Actual number missing", { PhoneNumber: { Code: var code } } when code < 0 => "WTF?", { } => null // no error }; if (error != null) throw new ArgumentException(error);
Пример синтетический — однако можно представить появление валидации такого типа (когда по отдельности два условия — ошибка, а вместе — нет). Скорее всего в этом месте в вашем решение появится nested if, а это очевидно увеличивает сложность.
Место — по всей видимости так-же не аргумент.
Подводя итог, я ни в коем случае не собираюсь навязывать свою точку зрения, однако призываю вас взглянуть на вопрос пошире — в том числе с моей точки зрения, и не быть настолько категоричным.
Нет он именно не читаем!Нет, он прекрасно читаем. Ваша неспособность легко разобраться с этим кодом не означает, что «насколько некачественный код продуцирует большая часть разработчиков».
— не нужно набирать тонну говноскобокВ Вашем коде 18 скобок.
Я заметил на своём опыте, а также косвенно на опыте коллег, что любой непривычный стиль написания кода кажется объективно нечитаемым и вызывает отторжение. Такое когнитивное искажение. Если некоторое время попытаться им пользоваться, несмотря на негативное отношение, искажение пропадает и можно пытаться проводить более объективное сравнение.
Скорее всего вам в качестве внеклассного чтения следует ознакомиться с понятиями функционального программирования.
Если у вас C# основной — тогда советую crash course F#.
Судя по тому, что в мире происходит, видны явные движения индустрии в этом направлении.
И ФП и ООП служат одной цели — управлению сложностью при написании программного кода, и они естественным образом могут сосуществовать, как это происходит в десятке современных языков. В отличие от вашей аналогии)
Сможет ли C# сообщество пойти по этому пути — я не знаю. Скорее всего сможет, потому что процесс начался ещё в .net 3.5 с появление linq, в отличие от Java мира, где было легче седлать Scala чем как-то трогать саму Java.
Так что теперь вопрос только за «программистами» — будут ли они достаточно гибкими.
Нормальный паттерн матчинг — это единственное нововведение, среди пречисленных в статье, которое у меня вызывает однозначно положительную реакцию.
У дизайнеров было два выбора: либо молча сглатывать неохват паттерна делая default-init (иначе говоря, возвращать default(T) из свича у которого не замэтчился ни один паттерн) или же бросать исключение. Микрософт выбрали второе...Во многих ФП языках ветка по умолчанию обязательна, и это тот третий и самый, на мой взгляд, логичный вариант.
Non-exhaustive patterns in case
.И в принципе понятно почему нет, если мы перебрали все возможные варианты (например, bool: true|false), то дефолтная ветка не нужна, соответственно нельзя ее сделать обязательной на уровне языка.
Но это проблему можно (и на мой взгляд нужно) решить через предупреждение компилятора.
там неплохой эвристический тоталити чекер
Он там был неплохой, а в какой-то версии его зачем-то переписали на прямой тупой перебор всех вариантов, и он стал очень тормозной.
Создаем ссылочную переменую и забываем проинициализировать. Что там будет храниться? Т.к. это ссылка, значит null, потому что дефолтное значение ссылки — null.
Делаем вывод, что
референсные типы являются nullable по определению
Создаем ссылочную переменую и забываем проинициализировать.Хммм. Ok.
Что там будет храниться?Ничего, потому что программа не скомрилируется и не запустится — чем такой ответ был бы плох?
Т.к. это ссылка, значит null, потому что дефолтное значение ссылки — null.А можно «мусок», как в C оставить. Или ссылку на какой-нибудь специальный синглтон.
Делаем вывод, что
Создаем ссылочную переменую и забываем проинициализировать. Что там будет храниться?
Есть языки в которых Вы не сможете забыть проинициализировать переменную ссылочного типа, если она явно не обявлена как nullable: Scala, Kotlin, TypeScript и C# 8 как пример, ваш код просто не скомпилируется.
На самом деле в C# нельзя забыть проинициализировать переменную даже если она nullable.
C# defines seven categories of variables: static variables, instance variables, array elements, value parameters, reference parameters, output parameters, and local variables.
Пруфлинк
Вы же не станете возражать, что забыть проинициализировать, например, instance variable можно? И при этом она будет автоматичестки инициализирована default-значением?
1. В подавляющем большинстве случаев (> 90%) nullabe типы не нужны. Это означет, что все было бы утыкано воклицателными знаками, как будто программа кричит на программиста.
2. Это привело бы к тому, что программисы просто ленились бы писать (!) и делали бы все типы nullable, что способствовало бы плохим практикам
Текущее решение было тщательно продумано и обсуждено с общественностью.
Сейчас же, многие просто не будут включать эту опцию в уже существующих проектах потому как понять, где нужно ставить “?”, а где не нужно, зачастую очень сложно.
где нужно ставить “?”, а где не нужно, зачастую очень сложно.
как это так? объясните. имхо, это же очень просто
class MyClass
{
public string Id;
public SomeType1 Property1;
public SomeType2 Property2;
...
public SomeTypeN PropertyN;
}
Глядя на этот класс я с большой долей вероятности могу сказать какое поле, скорее всего является, обязательным.
Какие поля являются «не обязательными» нужно выяснять разбирая код программы.
Теперь я включаю эту новую опцию, и получаю кучу предупреждений при попытке собрать проект. Дальше есть 4 варианта:
1) Забить на предупреждения (тогда зачем это все?)
2) Пометить все поля как "?" (тогда зачем это все?)
3) Отключить эту опцию (тогда зачем это все?)
4) Потрать кучу времени на рефакторинг работающей и отлаженной программы (здорово конечно, но надолго ли вас хватит).
это вопрос про настройку большого старого проекта. да, здесь единственный 100% точный вариант — это просмотреть код, чтобы знать, что nullable, а что нет. (хотя по хорошему уже бы иметь какие-то маркеры, комментарии там, или атрибуты, чтобы декоративно было все видно).
но если вы не хотите тратить время на подобную модернизацию большого и старого проекта, тогда зачем вы написали этот коммент?
Вы же понимаете, что «Ошибку на миллиард долларов» так просто не исправишь? Процесс будет долгим и сложным. Если включить nullable контекст во всем проекте, то программиста завалит предупреждениями, это уже обсуждалось неоднократно. К счастью, контекст можно включать для отдельных участков кода с помощью директив, это позволит постепенно перевести проект на новые рельсы, путем постепенного увеличения кода в non-nullable контексте. Всё учтено могучим ураганом.
но позвольте! а как вы раньше писали код, серьезно не задумываясь о том, что, в какой то конкретный момент, вот в этом вот месте, вы планируете возвращать null, и что вызывающий код должен сделать соответсвующую проверку? или вы просто по дефолту везде ThrowIfNull писали?
а что же касается "огромного количество фреймворков", то здесь ожидается полезность фунции только если конкретный фреймворк использовал это нововведение. скорее всего, MS введет атрибут [DontCheckForNullables] для референсов разных DLL.
Т.е. вот у нас функция принимает строковый параметр (без ?). Нужно ли проверять его на null? В текущей парадигме — да, в новой — нет. Хотя null потенциально может быть и там и там.
Текущее решение — не доделано, это костыль, чтобы улучшить проверки кода на потенциальные NullReferenceException. Но оно может привести к тому, что многие начинающие или низкоквалифицированные разработчики (коих хватает) будут считать, что в таких типах null не может быть никогда и это может привести не к уменьшению, а к увеличению этих самых NullReferenceException-ов, которые как известно, мы получаем в самый неожиданный и непредсказуемый момент (иначе бы мы их выловили бы еще на стадии тестирования).
Плюс '?' и так уже используется для nullable value types: int vs int?
int? = Nullable<int>
Для Reference Types, "?" — это просто подсказка компилятору + добавляется [NullableAttribute] (подсказка компилятору при испльзовании кода из других сборок)
Разница ещё в том, что в Value Types "?" логически относится к типу, а в случае с Refernce Types к переменной, полю или методу, так, что с точки зрения логики, надо было бы писать:
string myVariable? = null
a не
string? myVariable = null
Он и относится к типу, просто это тип анонимный. То, что тип реализован атрибутом, это уже детали реализации.
Нет, тип второй переменной — string?
Под анонимными типами в C# понимается совершенно другая вещь, например:
var myVariable = new {Field1 = "Field 1", Field2 = 2};
увидев такой код, компилятор создаст тип:
class ___AnonymusXXXX
{
public string Field1 {get; private set;}
public int Field2 {get; private set;}
}
Тут произошел конфликт названий, речь шла о другом виде типов. Статические типы (т.е. типы, известные компилятору) вовсе не обязаны совпадать с рантайм-типами.
Некоторые типы существуют только в исходном коде, а в рантайме полностью заменяются другими. Их-то ApeCoder и не вполне корректно обозвал "анонимными".
В языке C# уже были примеры таких типов — это dynamic
, заменяемый на object
; и кортежи, заменяемые на ValueTuple
с потерей информации об именах свойств. Теперь появился ещё один класс таких типов, только и всего. Но типами они от этого быть не перестали.
Теперь самый важный вопрос: что же поменялось в IL? С точки зрения исполняемого кода — ничего! Но с точки зрения метаданных поменялось конечно: теперь в типе который использует nullable аннотации все типы которые являются nullable проаннотированы атрибутом [Nullable]. Сделано это по понятным причинам: чтобы потребители вашего кода могли использовать ваши аннотации.
Этот string существует и в исходном коде и в рантайме.
По сути запись:
public string? MyProperty { get; set; } = null;
это синтаксический сахар для:
[System.Runtime.CompilerServices.NullableAttribute]
public string MyProperty { get; set; } = null;
Кстати, по поводу:
… заменяемые на ValueTuple с потерей информации об именах свойств
имена свойств не теряются, они сохраняются в метаданных.
Каким образом написанное вами опровергает тот факт, что string?
— тип?
имена свойств не теряются, они сохраняются в метаданных
Они пропадают из информации о типе. Был тип (foo: int, bar: string)
, стал ValueTuple<int, string>
но int? это действительно другой тип — Nullable.
Это как раз та путаница, о которой я говорил в предыдущих комментариях.
string? это тот же тип string, между ними нет разницы
Вы ходите по кругу. Почему между ними нет разницы, если для них определены разные допустимые операции (второму нельзя присвоить null)?
второму нельзя присвоить null
Очень даже можно, просто компилятор выдаст предупреждение, что эта переменная помечена как «Not Null», а вы пытаетесь присвоить эй null.
Сам тип string символ "?" никак не меняет.
Более того, если я попытаюсь использовать сборки скомпилированные в C#8 из C#7, то все поля помеченные как «string?» будут видны как обычные «string» и накаких предупреждений (или ошибок компиляции) не будет, если я попытаюсь присвоить им null.
А всегда ли это будет предупреждением, или это просто текущее состояние компилятора?
Более того, если я попытаюсь использовать сборки скомпилированные в C#8 из C#7
Естественно, что в разных языках разные системы типов.
Сам тип string символ "?" никак не меняет.
"Тип данных (тип) — множество значений и операций на этих значениях."
Так как набор операций изменяется, то это уже другой тип.
эта штука не меняет тип String, но она обозначает тип "nullable string" который может быть проассоциирован с переменной. Так же как и int[10] не меняет тип int, а делает новый тип по int и количеству элементов.
Предупреждения — это дань переходному периоду — фактически у этих конкретно типов пока более слабый контроль, но концептуально — это тип. Поэтому и синтаксис такой
string notNullable = "value1";
string? nullable = "value2";
if(nullable != null)
{
notNullable = nullable;
}
но зато:
int? nullable = 1;
int notNullable = 2;
if (nullable != null)
{
notNullable = nullable;
}
выдаст ошибку:
error CS0266: Cannot implicitly convert type 'int?' to 'int'. An explicit conversion exists (are you missing a cast?)
Вы так пишете, как будто смарт-касты в других языках ещё не изобрели...
Если бы «string?» был другим типом, то требовалось бы приведение типов
Ну, не факт, это зависит от языка, скажем, в F# нельзя неявно привести Int32 к Double, а в С# можно.
А что рефлекшн говорит по поводу «string?»?
А что рефлекшн говорит по поводу «string?»?
Что это обычный string
. А атрибут относится не к типу, а к переменной.
Я такое приведение могу сделать и для других типов определяя оператор «implicit».
А что рефлекшн говорит по поводу «string?»?
Ответ в статье:
Теперь самый важный вопрос: что же поменялось в IL? С точки зрения исполняемого кода — ничего!
Есть готовый синтаксис:
int i1 = 10;
int? i2 = null;
ну и нечего изобретать велосипед:
string s1 = "10";
string? s2 = null;
Кто учит язык и пока не знает, как это реализовано, тому и так нормально: суть похожая и внешне выглядит одинаково.
Кто уже знает, что там под капотом, тому тоже нормально: суть похожая и внешне выглядит одинаково, а что под капотом, он и так знает.
Поэтому он теоретически может быть отрицательным, хотя конечно в C# запись x[-1] лишена всякого смысла.
Не лишена.
Во-первых — можно перегрузить индексатор в вашем классе и корректно обрабатывать в нем отрицательные индексы.
Во-вторых, в C# массивы могут индексироваться не только с 0:
var x = (int[,])Array.CreateInstance(typeof(int), new[] { 42, 42 }, new[] { -1, 2 });
x[-1,3] = 1234;
А хотелось бы сказать компилятору «да я уверен, что там null не будет, не заставляй меня их инициализировать»
Для этого всего-то нужно инициализировать свойства в конструкторе...
Если инициализировать в конструкторе есть гарантия, что в уже созданном объекте не будет null, иначе состояние, когда есть объект и там null — допустимо (в процессе десериализации)
Инициализация в конструкторе это костыль
1. Я уже говорил, что эта инициализация просто нагружает GC лишний раз. Можно сказать что это короткоживущие объекты, но есть пункт 2
2. Может быть совсем не просто инициализировать свойство, которое суть сложный объект, да еще с приватным/интернал конструктором (сериализатору это без разницы, а вот вы будете в тупике)
3. И даже если можно, то в случае проблем с десериализацией вы можете получить неконсистентное состояние (новые данные + бесполезное дефолтные)
А теперь про гарантии. Вдруг десериализатор не заполнит некоторые свойства. Сравните:
а) В случае lateinit я просто сделаю проверку на null всех свойств 1 раз (и все) после десериализации. Я выполню гарантии в рантайме и у меня есть способ сказать компилятору «я сам».
б) в случае «дефолтные в конструкторе» я не смогу отличить что новое, а что старое. Мне придется делать полный валидатор, что уже перебор. Также он может быть невозможен, если значение свойства не отличить — и тогда придется все делать nullable. А это приведет к тому, что в 1000 мест мне придется проставлять!
Ожидалось, что шарп не будет изобретать свое, а возьмет удачные решения с других языков, но он взял только базовое и это явно недостаточно.
Возможно, он просто не успел. В Котлине это сразу было или потом добавили?
Логически правильно тогда либо не позволять создавать объекты такого типа никак кроме как десериализацией (и допустим вручную, но с заполнением всех полей). Либо десериализация возращает тип, который знает, что все поля заполнены. Хотя наверное, так все вообще сложно будет.
Пример реальной попытки: сериализатор требует пустого конструктора и заполняет только те свойства, что может заполнить (причем через internal сеттеры). Если не заполнены, то это нештатная ситуация и простой валидатор проверяет на null зная, что ни одно поле не может быть null. Сложный валидатор невозможен — ему тут не место. И все работает, все хорошо, только фатальный недостаток — не использует новых фич
Пробую #nullable. Инициализация дефолтными значениями не прошла. Сразу поломался валидатор на незаполненность, а больше ничего и не проверить. Попытка обвесить модельки #nullable disable тоже ничего не дало. В итоге сделал свойства моделек nullable и начал массово (реально в сотнях мест) проставлять! но на полпути задался вопросом «а зачем?»
Код остался прежним, но я получил требование в клинтском коде писать! (и необходимость помнить, что! обязателен, ведь non-null гарантирован логикой, nullable из-за компилятора), а вот выгоду я увидеть не смог
и придумать как переделать, да чтобы не городить жуткие костыли из-за слабости поддержки nullable в языке, я тоже не смог, поэтому вернул все обратно.
По-идее, клиентский код (в смысле business logic) не должен видеть все эти DTO, т.е. по хорошему у вас в доменной модели должен быть тип (скажем, class Person{}
), который обеспечивает все гарантии nullability и прочего, и который конструируется из DTO (скажем, class PersonDTO{}
) имеющий пустой конструктор, nullable поля и ноль гарантий, т.е. простой мешок пропертей. Валидация входных десериализованных данных происходит один раз в конструкторе Person(PersonDTO dto)
, а дальше гарантии распространяются через этот Person
.
--> json --> десериализация в PersonDTO --> Person(PersonDTO dto) -->
«десериализация в PersonDTO» может быть библиотекой-клиентом к некому апи, которую клиентский код может использовать. Она, можно сказать, транспортный слой, который при этом ничего не знает про business logic. И нормально, что этот слой валидирует полное получение данных, хоть и не может валидировать их суть. Я получаю ошибку сразу «fail fast», и к тому же на уровне, где она произошла.
Если я получаю из api погоду в виде иммутабельного объекта Weather c 3мя свойствами «город, температура, влажность», то рассматривать Weather как dto и создать WeatherModel, или просто использовать как есть? — каждый решает сам, но в случае «использовать как есть» у нас все равно есть гарантия транспортного слоя, что «город не null» и «температура настоящая, а не дефолтная»
И если переделка под nullable привносит только необходимость прописывать восклицательные знаки (или же отказ от проверок транспортного слоя), то я лучше откажусь от nullable
И нормально, что этот слой валидирует полное получение данных, хоть и не может валидировать их суть.
Ну вот я собственно и сомневаюсь в том что транспортный слой должен давать какие-то гарантии модели, помимо типа и формата данных (число, строка, дата, объект, массив и т.п.). Это как бы не его роль. Его роль — тупо сконвертировать данные из одного представления в другое "as is" и дальше дать решать бизнес-слою, полные это данные или нет. Может оказаться, что ему и не нужна абсолютная полнота, что он терпит частичные данные, и если у Weather не будет температуры, то и фиг с ней, он её не использует.
Тем более что частичные DTO — это распространённый метод обновления лишь некоторых выбранных значений вместо посылки полной модели..
Речь идет о сериализаторе. К нему есть требование дать гарантию, что он заполнит все поля в классе, тип которого ему передадут для материализации (и они не будут дефолтными). Это у него постусловия такие. И сериализатор достаточно глуп, чтобы отследить это прямо в процессе разбора данных. Приходится проверять постусловием
Эти постусловия четко отражены в спеке и следуя «концепции баррикады» можно не писать дополнительные проверки на это в клиентском коде и не получить NRE.
и с #nullable в их текущей реализации выбор очевиден
1 инициализировать дефолтные и поломать постусловия, от чего пострадает клиентский код (breaking change) — и все это с нулевой выгодой
2 заставить в клиентском коде проставить везде "!" и заставлять в новом коде это писать — и снова выгоды не вижу
3 оставить все как есть.
Сериализатор говорит: у меня Weather.City
всегда не null. И тут приходит HTTP PATCH
с частично заполненным JSON, в котором нет поля city
(потому что на клиенте оно не менялось). Какова реакция сериализатора? Выкинуть исключение, потому что, по мнению сериализатора, без city
этих данных недостаточно для работы? А с чего он взялся решать такие вопросы? А бизнес-слой считает, что ему данных хватает, но между клиентом и ним сидит чересчур умный сериализатор и не даёт данным дойти.
Это у него постусловия такие.
Постусловия можно поменять. Может вы имели в виду тех-задание, против которого нельзя возразить и ничего нельзя поменять? Ну да, с таким тех-заданием придётся выкручиваться.
И проблемы нет
А с чего он взялся решать такие вопросыИменно так. Конечно можно сказать, что не дело сериализатора проверять заполненность и пусть проверяет уже на месте. Но если апи четко объявляет, что такого не будет (спека), то нарушение постусловия означает, что система работает неправильно.
Допустимо ли это и пропустить дальше или упасть «fail fast» зависит от приложения. Все эти «fail fast» обычно все исчезают в процессе разработки и тестирования. И если далее такое будет, то это ну совсем нештатная ситуация, которая требует немедленного внимания, потому что логике быть не должна ни в коем случае.
И да, поле с возможностью null будет nullable (или помечено аттрибутом, раз уж #nullable не зашел)
Постусловия можно поменятьНельзя. Если имеется код, который завязан на контракт (спеку) кода сериализации, то ничего менять уже нельзя.
зы: конечно вы правы про то, что «каждый должен заниматься своим делом». Но подход fail-fast себя оправдывает, если ошибки, которые он ловит, можно починить до релиза. Если же это сервис, который должен быть устойчивый ко всем «а что если», то там предусматриваются все варианты, а не «зови программиста»
Так как у вас на выходе "сериализатора" не DTO, а фактически частично валидированные модели с частичными гарантиями (и у вас выходит уже не совсем сериализатор, а целый API), то если вам хочется донести эти гарантии до пользователя, вам придётся делать промежуточный слой из настоящих DTO, например:
JSON --> JObject --> new Weather(JObject dto)
И тогда Weather может спокойно сделать свои поля non-nullable, а все проверки держать в своём конструкторе (а если валидация сложная и в один конструктор не влазит, то в фабрике).
В C# не завезли Optional/Maybe из коробки :(, поэтому популярные библиотеки сериализации работают либо со строго-типизированным POD через рефлексию, либо создают dynamic
(объект "динамического типа", который создаётся в рантайме — примерно как Object
в JavaScript), либо выдают значения через какую-нибудь кастомную обёртку вроде упомянутого NewtonSoft JObject
. Последние два способа — это фактически Dictionary<string,object>
под разными соусами, и наличие/отсутствие значения проверяется через наличие/отсутствие ключа. Первый способ — более строгий, но без Optional
в стандарте приходится извращаться либо со "специальным значением" (0, "", null, default(T)) в случае отсутствия поля, либо вводить дополнительное булево свойство вроде:
[XmlElement(IsNullable = false)
public string City;
[XmlIgnore]
public bool CitySpecified;
Такая вот попоболь.
Не очень понял, за что минус. Конечно, вы можете написать свой Optional и свой сериализатор (как я сделал для своих задач), но это уже велосипедостроительство. Будет сильно лучше, если эти типы будут присутствовать в стандартной библиотеке в пространстве имён System.
Чем именно вот это https://github.com/microsoft/referencesource/blob/master/mscorlib/system/nullable.cs отличается от option?
Тем, что where T : struct
.
Ну, а для ссылочных типов null есть. Фактически для них только Option был до 8.0
Но Nullable Reference Types — это какой-то перебор и шаг не туда.
Это очень странное решение, которое выливается в множество неочевидных нюансов, включая обратную совместимость с существующим кодом, отсутствие поддержки в runtime (хоть это и обещают сделать в следующих версиях, интересно как?) и многое другое. Уже сейчас, когда новой версией языка по сути еще никто не пользовался, это нововведение уже вызвало некий раскол среди разработчиков, т.к. плюсов и минусов у этого решения хватает.
Интересно, что Nullable Value Types (int?) такой реакции не вызывали, они очень органично «зашли» в язык.
и «поддержка в runtime» не нужна. Ведь основная задача этой функциональности — позволить на этапе написания кода избегать NRE с нулевой стоимостью во время выполнения.
Просто «поздно решили внедрить» и поэтому приходится делать это костыльно
Undefined behavior в C — это совсем не то. Это когда are[-1] в коде может привести к чему угодно — от вычитывания любого элемента из массива, до восстания SkyNet.
У вас же — возможно surprising behavior, но ни как не undefined.
После прочтения поста и комментов, создалось впечатление, что комментаторы — профессионалы, которые конечно же прочитали все треды по обсуждению новых фич на гитхабе и не хуже разработчиков знают какие новые фичи нужны в C# и какой синтаксис у них должен быть, который не повлияет на обратную совместимость.
А вот, например, паттерматчингом я никогда не пользовался (разве что is null вместо == null) и вообще не вижу зачем он, но мб зачем-то и нужен.
хотя конечно в C# запись x[-1] лишена всякого смысла
Если речь только только про массивы, то да. Если про C# вообще, то нет. Смысл зависит от того, что есть x. Самописный индексатор можно делать всё что хочет. Даже без самописных индексаторов я собственными руками писал x[-1], где x был указателем.
Встроенные типы, такие как массивы или строки, конечно же поддерживают индексер (operator this[]) для типа Index, так что смело можно писать
var items = new[] { 1, 2, 3, 4, 5 }; items[^2] = 33; // 1, 2, 33, 4, 5
Проверяю:
var items = new[] { 1, 2, 3, 4, 5 };
Print(items);
items[^2] = 33;
Print(items);
0) 1,2,3,4,5
1) 1,2,3,33,5
Microsoft Visual Studio Community 2019 Preview
Version 16.2.0 Preview 1.0
NET Core 3.0.100 Preview 5
There is no compatibility guarantee from one C# 8.0 preview to another.
In short, if you use C# 8.0 preview in Visual Studio 2019, some features and behavior may change between now and when C# 8.0 fully releases.
Источник: habr.com/ru/company/microsoft/blog/443000
Это, кстати, соответствует майкрософтовской документации (см мой коммент выше), что ^0 это индекс за-концом массива (а не последний как в статье), ^1 это последний элемент, а ^2 предпоследний.
Для любого числа n индекс ^n совпадает с sequence.Length — n
Возможно, в предыдущих Preview было иначе.
Представьте массив x = {1, 2, 3}. Если взять x[0..2] вы получите {1, 2}, а вот если взять x[..2] или например x[..], вы получите {1, 2, 3}.
Проверяю:
// Представьте массив x = {1, 2, 3}.
var x = new[] { 1, 2, 3 };
// Если взять x[0..2] вы получите {1, 2}
Print(x[0..2]);
// а вот если взять x[..2] или например x[..], вы получите {1, 2, 3}.
Print(x[..2]);
Print(x[..]);
0) 1,2
1) 1,2
2) 1,2,3
дизайнеры всех современных языков «продолбали» как минимум несколько важных аспектов [...] zero-terminated strings, когда длина строки вычисляется за О(n).Ну, не так уж и всех, а лишь тех, кто решил не заморачиваться с нормальным строковым типом и обойтись
char[]
. В любом приличном современном языке (да и не только современном, вспомните тот же Паскаль) строки — один из базовых типов, который неплохо оптимизирован (во всяком случае, для более-менее коротких строк никто длину не вычисляет).В принципе, попробовал все перечисленные фичи.
Мнение:
- nullable — сделано совсем через задний проход. Если по дефолту сделать nullable, в происходящем появится хоть какой-то смысл. Имхо сильно на любителя.
- Index & Range — звучит безумно, но пальцы привыкают быстро.
- Default interface members — минимум 90% разработчиков это не понадобится никогда. И слава богу.
- Pattern matching — ура. просто ура.
Если по дефолту сделать nullableНу так оно и так во всем существующем коде по дефолту nullable. И все эти годы так было. И именно это «по дефолту null и инициализации не было и никто не подсказал про забытую инцицализацию» и является основной причиной ошибок NRE.
Проблема NRE в том, что вроде есть стектрейс, но он бесполезен, потому что я не знаю через сколько уровней и откуда пришла переменная, а воспроизвести не получается. А так если бы каждое место не допускало null, то мне бы еще компилятор указал где это и это было бы далеко от места реального NRE
А так лучше бы они records сделали — крайне полезная вещь.
Лучше бы они records сделали
Это да. Я очень расстроился, когда понял, что все слухи о них оказались слухами.
А по поводу nullable моя претензия заключается в том, что данные, приходящие из legacy (#nullable disable) кода, почему-то считаются non-nulable, хотя обычно это не так. И за счет этого мечта о "прощай, внезапный NRE" остается мечтой.
У меня реакция такая:
- Nullable ― Ура! Наконец. Теперь ещё период всеобщей адаптации пережить и будет счастье.
- Index & Range ― Кому-то реально не хватало? Ну ладно, пусть.
- Default interface members ― whatever
- Pattern matching ― За if (x is string s) большое спасибо, а для чего всё остальное?
- Switch expressions ― Спасибо.
- Async streams ― Может быть, когда-нибудь.
- Using declaration ― Кое-где бы пригодились, а то иначе некрасиво.
- Static local functions ― Предположим, кому-то это принципиально.
- Disposable ref structs ― Кому-то точно это принципиально.
Родному F# трудно играть на одном поле с C#. Всё взлетает, взлетает, никак не взлетит. Последний недостаточно плох, чтобы с него дёргаться. Котлин, на сколько я его себе представляю, это что-то а-ля C# или нечто среднее между C# и F#, лишь бы забыть про Яву.
Котлин под дотнет мог бы быть способом перетащить людей с платформы Java на Net. Кому-то это надо?
Я думаю, также C# недостаточно плох по сравнению с Котлин тоже. Так что котлину придетбя бороться с C# для которого есть большая поддержка
Проблема развития старых языков, в т. ч. c# — в необходимости сохранять обратную совместимость. Поэтому новые фичи (e. g. Nullable references) выглядят как костыли, а язык превращается в монстра(полюбуйтесь на с++).
Какими вытекающими? JetBrains сделали Rider — конкурент VS, причём успешный.
По-вашему под clr, кроме шарпа, не может быть юзабельных языков?
Юзабельные представить можно, тем более что в NET они есть. Юзабельные и не нишевые ― не очень представляю. И не очень представляю мотивацию, зачем одним упираться делать такой же, но другой язык (плюс инструментарий, без которого он сразу никому не нужен), а другим его использовать вместо существующего мейнстримового C#. Если бы последний был уродцем, которого все просто терпят до появления альтернативы, то да. А так нет.
Rider ― это ещё одна хорошая IDE для NET. [Глупая_аналогия]Открыть ещё одну конкурирующую булочную, думаю, сильно проще, чем конкурировать с самим хлебом.[/Глупая_аналогия]
Практика показывает, что Котлин вместо мейнстримовой Джавы, которая тоже не урод и активно развивается, очень даже заходит.
Я про Яву слышал другое.
В случае с же CLR и C# все популярные платформы контролируются Microsoft, которому продвигать что-то, кроме C# — совершенно неинтересно.
Почему неинтересно? А если это сделает разработку под их экосистему комфортнее?
In the previous preview, the framework supported Index and Range by providing overloads of common operations, such as indexers and methods like Substring, that accepted Index and Range values. Based on feedback of early adopters, we decided to simplify this by letting the compiler call the existing indexers instead. The Index and Range Changes document has more details on how this works but the basic idea is that the compiler is able to call an int based indexer by extracting the offset from the given Index value. This means that indexing using Index will now work on all types that provide an indexer and have a Count or Length property. For Range, the compiler usually cannot use an existing indexer because those only return singular values. However, the compiler will now allow indexing using Range when the type either provides an indexer that accepts Range or if there is a method called Slice. This enables you to make indexing using Range also work on interfaces and types you don’t control by providing an extension method.
Отдал был часть кармы если бы Хабр позволял «краудфайндить» на статьи.
Если бы проще заявили «немного полезного синтаксиса над рефлекшном» я бы не мучился, а так остается недоумение — что я упускаю.
Вопрос:
foreach (var i in 0..10) { }
Это не работает, потому что у Range отсутствует энумератор. Его ещё не сделали или его не собираются делать?
Можно спросить по-другому. Такой foreach компилятор мог бы заменить на for/do/while безо всяких энумераторов. Спецификация foreach может быть расширена на нативную поддержку Range. Компилятор ещё не умеет это делать или такой поддержки не было в планах?
foreach (var i in ..)
Но код выше ↑↑↑ ничего не значит, если он не ассициирован с каким-либо контейнером. Чисто теоретически конечно можно было бы на этом месте компилировать не просто Range а какой-нибудь DefiniteRange, но для этого нужно натренировать компилятор чтобы он понимал, что ни первый ни второй аргумент Range-а не являются «с конца».
В 2017 году были разговоры об этой фиче, а потом что-то утихло…
Default interface impl блокирует интероп с жабой и свифтом, а у разрабов на этот интероп мега-планы (см. .NET 5). Оно добавлено не потому, что в самом языке нужно (очень многие были против), а потому что в остальных языках есть, и с ними надо нормально взаимодействовать.
А вот extension everything — по большей части синтаксический сахар, хоть и очень приятный. Не горит, скажем так.
Разрабы шарпа не тянут больше одной мегачифи за версию (если не считать первых версий): 3 LINQ, 4 dynamic, 5 async, 6 Roslyn, 7 pattern matching, 8 null ref types.
Оставшиеся запланированные мегафичи: concepts, records (сейчас отмечены как 9.0), extension everything, original/replace, discriminated unions (сейчас отмечены как X.0).
Короче, если повезёт, лет эдак через пять могут добраться до этой фичи. Поля, кстати, будут вряд ли. Их можно реализовать (без синтаксиса — уже сейчас), но при широком применении это убьёт GC.
Варианты [0..2] и [..2] дают одинаковый результат: 1, 2
Может это был баг?
Что нового в C# 8?