Pull to refresh

Comments 230

Так и хочется сказать «астанавитесь». Я конечно люблю c#, но что-то становится уже страшновато)

Да фичи то ещё ладно, а вот синтаксис для них они выбирают какой-то слишком странный.
В 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 юзать

Вы исходите из ложного предположения, что индексы начинаются с нуля.
А ещё возможны приколы, когда вместо оэтдаемого IndexOutOfRangeException будет какой-то другой функционал.

А когда они не с нуля? (в шарпах не шарю)

Можно создать массив с ненулевым начальным индексом (например начинающийся с 1 и длиной в 2 элемента) Array.CreateInstance в помощь. Тогда Exception будет бросаться одинаково при доступе к 0 и к 3 элементам.

А что тогда будет делать ^0 ?

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


А x?. по мне всё равно дико смотрится, особенно для тех, у кого есть опыт работы с Ruby, где существует совершенно логичная конвенция, что x? всегда возвращает boolean, ну типа active? вместо isActive. Думаю, всё равно в итоге нормальную Maybe монаду завезут и эти? после x станут deprecated.

?.. Присутствует и в других языках программирования и делает то же самое
В kotlin подобный же синтаксис. А больше всего раздражает там, когда вызов или свойство класса из java библиотеки идёт, везде эти вопросы тыкать надо. По мне, ну не вылетает исключение, но все равно, при необходимости выполнение идёт не в то русло и попробуй отловить. Спорная это вещь. И думаю они явно на kotlin смотрели.

Вы давно не смотрели в kotlin. Давно же появились платформенные типы — String!, которые могут использоваться как nullable, как и non-null.

Извиняюсь, а это зачем? Чтобы нельзя было засунуть null в non-null? Но если он «как nullable, как и non-null», значит в него в любом случае можно сунуть null? Или это у них такой аналог аннотации @NonNull? Не знаю, по мне дак аннотации намного нагляднее, чем вспоминать что означает тут восклицательный знак, то ли указатель какой, то ли факториал вообще.

Не, этот тип применяется только когда nullability неизвестна (когда дёргаем java код, на котором нет никаких аннотаций). Тогда компилятор позволит работать с этим типом как с nonnull, но везде натыкает проверок на null для early null propagation. Т.е. когда есть аннотации — то в Kotlin всё будет хорошо и везде будет нужный тип. Если нет (старый код или просто не заморачивались) — то можно не писать портянки из всяких ?., ?:,!!! и прочего и просто писать код, не столь безопасный (хотя если где-то прилетит null, где вы его не ожидали — то вылетит как можно раньше, а не когда этот null расползётся по всем структурам), но без манускриптов из символов и просто красивый.

А, ну понятно, то есть что-то на подобии принудительной проверки на null получается? Вообще проблема аннотаций в Java состоит в том, что они не обязательны, я бы вообще их отсутствие Fatal Error бы сделал, ибо помимо @NonNull существуют и @ Override, которые оооочень помогают, но которые тоже не обязательны, увы. В Котлине-то хоть они обязательны?) А-то ведь шило на мыло менять)

Ну nullability в kotlin на уровне типов, override не аннотация, а модификатор функции, его отсутствие — ошибка компиляции.


Посмотреть на разные особенности языка можно онлайн, например на https://play.kotlinlang.org/koans
Просто выбираете нужную тему и смотрите, как она реализована в данном случае.

Вот в том-то и дело, что в Java override это именно аннотация, причем не обязательная, даже не знаю почему произошло такое недоразумение. А за ссыль спасибо, то, что нужно, а-то все вокруг говорят, а чтобы пощупать — это проекты готовые нужны, ибо хеллоуворлда недостаточно.
Не понимаю, почему его все так любят? Прям реально в комментах пишут какой Котлин классный. Как будто-бы до этого они ни на чем не писали и это их первый язык.

Как и со всеми языками — нужно пробовать, желательно не на хеллоуворлдах.
А вообще он решает много попоболи из Java, простой (относительно какой-нибудь Scala например), фичастый (корутины, инлайновые классы вон завозят потихоньку и т.п.), ну и с недавних пор мультиплатформенный (jvm, js, native).


Не помню кто сказал (кажется это был Андрей Бреслав), когда пишешь на Котлине — прям на душе приятно. Ты не пишешь очередные геттеры, а пишешь код.

Да это понятно, я и не спорю что Java весьма многословна, тут я отрицать не буду, и необходимость в более лаконичном языке назрела давно, просто можно было бы оставить человеко-понятный синтаксис, как например Ман для библиотеки корутин для Java, а не городить поверх Java свое. Такое ощущение что они специально творят такую дичь :/

Поэтому по мне все эти восторженные возгласы исключительно из-за появления современного и модного языка, который развивается и в котором уже есть почти все в отличии от других более старых ЯП, для которых многие библиотеки уже и вовсе не развиваются. Типа о, он даже лямбды из коробки поддерживает, вот это да! А корутины? Скоро будут? Вот это сервис, прям индивидуальный подход!) Я так это вижу…

P. S. Но по поводу геттеров и сеттеров посмотрю, спасибо, это проблема многих языков на самом деле и несколько изматывает при создании нового класса все это реализовывать вручную, а они это как реализовали?
Потому что в андроиде ооочень старая Java. А Котлин — хороший современный язык.
Ну я думаю язык надо выбирать в плане синтаксиса, а не новизны.
и андроидовская джава очень многословна и не отвечает требованиям к современным языкам.
В D вроде бы используется arr[i] для индексирования с начала и arr[$-i] для индексирования с конца. То есть внутри контекста скобок доступна длина массива в виде символа $.
А тут можно было бы ~0

~0 вернёт обычный int. Это будет являться проблемой, т.к. усложнит индекацию и поломает существующий код, где индекс не является неотрицательным и начинается с нуля. Должен возвращаться объект типа Index, чтобы всё работало корректно.


^0 как автор в статье написал, лучше бы для возведения в степень использовали.

Оператор ^ уже используется для операции XOR. Его нельзя использовать для возведения в степень. Так как унарного ^ не было, его решили использовать таким образом — всё в порядке.


А для возведения в степень разумным выглядит такая конструкция: a ** b.

Ничего страшного. Указатели в C# — штука крайне редкоиспользуемая, чтобы отказываться от такого функционала.

Зачем для этого вообще отдельный термин?

Как минимум по бюрократическим причинам. В рамках 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.

Это действительно так, и это действительно некрасиво с точки зрения математики. Но понятно откуда это пошло — с С++овских итераторов begin() и end(), а там end() указывает за конец коллекции, а не на последний элемент, потому что иначе не сделать итерацию коллекций нулевой длины. Вот в Swift сделали оба вида диапазонов — и закрытые, и полузакрытые. Вполне логично и удобно, наличие обоих вариантов подчеркивает разницу между ними и уменьшает путаницу. Две точки для полузакрытых, три точки для закрытых. Можно даже мнемонически запомнить — три точки больше, значит и диапазон больше (включает последний элемент).
Да уж. Змея сожрала свой хвост. Вообще-то этот вопрос уже исследовался много десятилетий назад. Тем самым Дейсткрой, который, как известно, не программист, потому что свои статьи не на компьютере набирал.

Но, конечно, Apple не может же использовать опыт того самого Xerox PARC, с которого они слизали интерфейс MacOS (оригинальной версии, не MacOS X).

А так — полузакрытые интервалы и нумерация с нуля — это просто удобно. Может быть не слишком привычно для человека без опыта, но удобно.

интересно в каком языке все таки сделали полное математическое описание, с круглыми и квадратными скобками?

Mesa. 1976й год. Собственно на опыт работы с Mesa обсуждение, ссылку на которое я давал выше, и ссылается.

Вот всё как вы хотели: и все четыре варианта предусмотрены и да — таки круглые и квардартные скобки. Но «новое поколение» хочет, видимо, лично понаступать на те же грабли. Языки, созданные до их рождения, видимо «неправильные» — наверное за полвека что-то кардинально новое в математике случилось…
Нет, вы мне ответьте, как это читать? 2..7 — это «взять с третьего и не доходя до восьмого элемента»? В чём смысл такой записи? Ну то есть я вижу один смысл — когда у нас есть либо длина, либо «указатель на конец». Но мы берём срезы списков не только «до конца». Может быть, описание открытых интервалов само по себе и имеет смысл (хотя лично мне он неведом, это же целые числа в любом случае), но для срезов…
В чём смысл такой записи?
Вы в каждой строчке программы хотите увидеть «смысл жизни»? Или как?

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

И это всё совершенно не зависит от того — рассматриваете ли вы вещественные числа или целые.

Может быть, описание открытых интервалов само по себе и имеет смысл (хотя лично мне он неведом, это же целые числа в любом случае), но для срезов…
Вот именно для срезов полуинтрвалы и лучше. Потому что вы их делаете не для того, чтобы на доску повесить, а для того, чтобы какой-то алгоритм реализовать. И вот там +1/-1 встречается реже, если использовать полуинтервалы.

Как уже упоминалось: попытки использовать другие варианты в истории были. Mesa вообще все четыре варианта поддерживала. Но оказалось, что это — хуже: если у вас в 70% случаев полуинтервалы, а в 30% случаев — что-то другое (да, иногда могут и отрезки и даже интервалы оказаться удобными), то в этих случаях возникает путаница уже из-за того, что человек забывает, что вот конкретно здесь у него не полуинтервал, а что-то другое. Лучше в этих [относительно редких] случаях +1/-1 написать…
Надо посмотреть завтра на свой код. Какие срезы и как я там использую. Пока что мне кажется, что я всегда знаю номер конечного элемента, а не следующего за ним.
Но вы так убедительно говорите, что может быть, я просто привык.
Вот только что работал со срезами. Разделить массив на две части по накопленной сумме. То есть я ищу номер первого элемента (назовём его limit), при котором сумма станет не меньше установленного значения, а затем разделяю массив на две части — к этому элементу и после него. Получается, с этим синтаксисом мне надо будет писать два раза limit+1 — для первой части, потому что интервал полуоткрытый справа, и для второй части — потому что вторая часть начинается после limit.
Вы как хотите, но мне это по-прежнему кажется некрасивым. Тогда уж вводить синтаксис для закрытой и открытой границы — причём любой, правой или левой. Тогда было бы удобно — я писал бы что-то вроде [0..limit] и (limit… array.size()).

Если limit оказался за правым краем (limit == count), то [0..limit] вылетит без видимых причин. "Красивый код" обезобразится некрасивый проверкой, которая будет торчать посреди красивого кода камнем посреди пустыни. А код с явным добавлением единицы органично требует такой проверки, явно указывает на её необходимость, так что вы про неё, во-первых, не забудете, а, во-вторых, не будете плеваться, натыкаясь на неё глазами потом.

Факт достижимости нужного результата на списке нужно в любом случае проверять так или иначе. Независимо от использования срезов.
И я, честно сказать, не понял, чем тут помогает ±1 в десяти местах.
То есть я ищу номер первого элемента (назовём его 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", то внутренне напрягись и жди ошибки.

В чём смысл такой записи?

  1. Попробуйте взять пустой интервал если запись a..b эквивалентна отрезку [a,b], а не полуинтервалу [a,b). Отрезок [a,a] непустой, он состоит из 1 элемента a. Писать a..a-1, что ли? Или a+1..a? Для полуинтервалов же это не проблема, [a,a) — пустой.
  2. Попробуйте взять два смежных отрезка. [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, то мне нужно, чтобы множество всех возможных подпоследовательностей включало в себя и пустые, для полноты и общности.

UFO just landed and posted this here
UFO just landed and posted this here

Стареете, хуже воспринимаете нововведения — это нормально.

UFO just landed and posted this here
Например Реймонд Ваган Дамадьян, изобретатель МРТ, ему сейчас около 80 лет.
И что он такого сделал «прорывного» в 5 лет? Когда он свою первую статью про МРТ публиковал — ему 35 было. А IT… В основном сейчас на Big Data молются и прочие всякие Map Reduce. Джеффу Дину, когда он это всё придумал — было 32. И это — в общем типичная история: случаи, когда «седовласые дядьки за 50» что-то реально изобретают (а не приписывают себе что-то, что сделали другие) — случаются, но их очень-очень мало.

Именно они стоят у руля лабораторий, которые дают вам технологие, на которые вы молитесь.
Стоять у руля != изобретать. Многие изобретения вообще случаются потому, что учёные, которые их делают всё проводят «мимо» руководителей.

И это конечно факт прискорбный, что «истина недоступна большинству», но ожидаемо — так что по барабану.
Как раз большинству она доступна. А вам — похоже, что нет.
UFO just landed and posted this here
А сколько Торвальдсу когда линукс стал мейнстримом и реально вошел в жизнь каждого?
А причём тут это вообще? Торвальдс уже давно код сам не пишет и, фактически, играет роль того самого «заведующего лабораторией». Под руководством которого другие люди занимаются инновациями. Последняя действительно прорывная вещь, которую он сделал — это был Git. Что случилось больше 10 лет назад и было ему тогда, оп-па, таки 36.

И я привел парочку примеров в подтверждение (помните чего? именно неверности «Стареете, хуже воспринимаете нововведения»).
Это где, извините? Всё что вы привели — несколько примеров того, как человек в 20-30-40 лет что-то сделал — что захватило мир гораздо позже. И те же самые люди, которые в молодости «переворачивали мир» — в старости начинают, зачастую, выглядеть чудаками. Как тот же Столлман, который реально очень сильно изменил путь всей индустрии когда-то… а сегодня отказывается пользоваться браузером, чем всех умиляет.

Потому что он утверждает, что знает, что именно определяет мое мнение.
Нет, он просто шутит. А вот уже то, что вы безобидные шутки воспринимаете «в штыки» — заставляет хлопнуть себя ладонью по лбу и громко расхохотаться.

И Аристотель, и многие ученые мужи современности, и собственно лично я, считаем именно так. Вы конечно можете думать как вам вздумается. Но на этой двойственности мнений мы и закончим с этим пунктом.
Ну, Аристотель — он, конечно, дядька умный, да. Но вот вы ответьте на один вопрос: если вам нужно будет количество лап у мухи узнать — вы просто возьмёте и посчитаете или будете тоже схоластику разводить?
UFO just landed and posted this here
Но это же не означает, что подобный код перестает быть Г.

Нет, не означает, но он и в самом деле перестаёт быть этим самым Г.


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


Вот чтобы обсуждаемый код перестал быть "Г.", требуется именно нулевой рефакторинг, и большое количество людей в отрасли его уже проделали.

Дело не в том, функциональный он или нет, а в том, что он абсолютно нечитаем, его будет тяжело сопровождать.

А как вы определили, что он "абсолютно не читаем", а не просто "непривычен"?

UFO just landed and posted this here
— этот код легко читается
С чего вы взяли? Кто вам сказал что он легко понимается всей командой разработчиков?

Так какая выгода от такого матчинга? Один вред.
Реализация функционального подхода наверное.

— не нужно набирать тонну говноскобок
Нужно набирать тонну говно-if'ов

— есть возможность использовать разные типы исключений
Кто вам запрещает вместо строки возвращать разные исключения?

легко проверяется во время дебага (даже точки останова ставятся элементарно)
Unit test, не слышали?

Внезапно у вас поменялась структура объекта, который надо валидировать, и вы радостно переписываете все свои if'ы.
UFO just landed and posted this here
но обсуждаем то, что в статье,
Да и что вы будете делать, если...
Как мне кажется это взаимно исключающие вещи.
если вам так будет легче, то в рассматрвиаемом примере кода, реализация функционального подхода — вред для читаемости и сопровождения.
Не вы вызывает затруднений у меня. И как бы это же синтетический пример, вы помните?
А что такое читаемость я сейчас расскажу:
Серьезно?
В своей работе, разработка приборов учета и ПП безопасности, несколько лет успешно используем ДРАКОН. Для нас код вторичен.
там словесный понос.
В вашей комнате разработчиков это допустимая словесная форма?
UFO just landed and posted this here
этот код легко читается

Почему вы считаете что он читатся легко? Рациональные аргументы?


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

Расскажу как для меня видятся эти два кусочка кода, возможно проследив за моим ходом мыслей вы поймете, что бывают люди, думающие по другому, и это тоже правильно (а может быть и более правильно):


  1. Код с матчингом читается легче, потому что все ветки находятся на соседних строчках, друг под другом, и их легко визуально сравнивать. Тот самый ваш пример про два хеша — тут то же самое. Код с if — разделен блоками кода — это в некоторой степени засоряет визуальное восприятие. Однако главное не это.
    Код с шаблонов описывает сам себя — он декларативен, он как две картинки с отличиями, которые дети сравнивают в детском саду. Это просто как дышать. Код же с if — императивен. Мне надо загрузить строчку сравнения в "оперативную память", чтобы понять что именно там сравнивается. А чтобы сравнить два условия — мне вообще необходимо нагрузить свой мозг. Сравните это с "в верхней строчке после двоеточия null, а в нижних — что-то… все понятно".


  2. При дебаге кода с сопоставлением шаблона, я ставлю одну точку останова — в месте инициализации person, затем вывожу его визуальное представление и визуально сравниваю с шаблоном. Один раз делаю операцию из детского сада. Я точно уверен, что внутри кейса person никак не изменится — мне вообще не нужно жать step forward, потому что это всего один шаг.
    Сравните с кодом с if, где в принципе объект person может поменяться по дороге (по крайней мере теоретически). Кроме того, я скорее всего буду жать step forward 4 раза, и сравнивать конкретные вещи 4 раза — в первом случае это person с null, потом PhoneNumber с null и так далее. По-моему, когнитивная нагрузка и временные затраты в этом случае больше.


  3. Набирать много скобок или много if — это в принципе дело вкуса, и врядли аргумент. Если бы это было важно для вас, вы бы программировали на Питоне. А тут всетки не Лисп, чтобы не это ругаться. Выглядит как обычный json.


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


    { PhoneNumber: null } => new ArgumentException("Phone number missing entirely"),
    ...
    throw error;

    Вот только в этом коде и Вашем есть большая разница: Ваш код нарушает базовые принципы структурного программирования. Наличие более одной точки выхода из блока кода значительно увеличивает когнитивную нагрузку (надо держать в голове где там оно могло прервать поток исполнения) и приводит к багам при дальнейшем изменении этого участка кода. 4 throw это 4 goto по своей сути. Возможно, программисты C# привыкли к такому, и это наоборот best-practise, но я предпочитаю в этом вопросе следовать Дейкстре неукоснительно.


  5. По-моему все ровно наоборот. Предлагаю вам не изменяя целиком ваш блок, добится того же, что и я, при добавлении одной строки:


    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, а это очевидно увеличивает сложность.


  6. Место — по всей видимости так-же не аргумент.



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

Нет он именно не читаем!
Нет, он прекрасно читаем. Ваша неспособность легко разобраться с этим кодом не означает, что «насколько некачественный код продуцирует большая часть разработчиков».
— не нужно набирать тонну говноскобок
В Вашем коде 18 скобок.

Я заметил на своём опыте, а также косвенно на опыте коллег, что любой непривычный стиль написания кода кажется объективно нечитаемым и вызывает отторжение. Такое когнитивное искажение. Если некоторое время попытаться им пользоваться, несмотря на негативное отношение, искажение пропадает и можно пытаться проводить более объективное сравнение.

UFO just landed and posted this here

Скорее всего вам в качестве внеклассного чтения следует ознакомиться с понятиями функционального программирования.
Если у вас C# основной — тогда советую crash course F#.


Судя по тому, что в мире происходит, видны явные движения индустрии в этом направлении.

Не вижу почему такие тренды и «явные движения» должны приводить к смешению двух языков в один. Если у вас есть лучшие в мире пылесос и телевизор, разве это автоматом значит, что пылесосу к месту придется 40" экран, а телевизору — мешок с мусором?

И ФП и ООП служат одной цели — управлению сложностью при написании программного кода, и они естественным образом могут сосуществовать, как это происходит в десятке современных языков. В отличие от вашей аналогии)


Сможет ли C# сообщество пойти по этому пути — я не знаю. Скорее всего сможет, потому что процесс начался ещё в .net 3.5 с появление linq, в отличие от Java мира, где было легче седлать Scala чем как-то трогать саму Java.


Так что теперь вопрос только за «программистами» — будут ли они достаточно гибкими.

В одном проекте отлично могут сосуществовать .cs, .fs и .il файлы и/или библиотеки. Лично мне кажется, что пытаться слепить их все в одну сущность — откровенно неудачная идея. Время покажет, конечно.
Вы сравниваете изначально разные вещи, с различным предназначением. Для ЯП нормально взаимствовать друг у друга.

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

Хотя есть один момент, что расстраивает:
У дизайнеров было два выбора: либо молча сглатывать неохват паттерна делая default-init (иначе говоря, возвращать default(T) из свича у которого не замэтчился ни один паттерн) или же бросать исключение. Микрософт выбрали второе...
Во многих ФП языках ветка по умолчанию обязательна, и это тот третий и самый, на мой взгляд, логичный вариант.
UFO just landed and posted this here
Вообще согласен — такого действительно нет. Хаскель, кстати, тоже ошибку кидает Non-exhaustive patterns in case.

И в принципе понятно почему нет, если мы перебрали все возможные варианты (например, bool: true|false), то дефолтная ветка не нужна, соответственно нельзя ее сделать обязательной на уровне языка.

Но это проблему можно (и на мой взгляд нужно) решить через предупреждение компилятора.
UFO just landed and posted this here
там неплохой эвристический тоталити чекер

Он там был неплохой, а в какой-то версии его зачем-то переписали на прямой тупой перебор всех вариантов, и он стал очень тормозной.

чем выше версия C# тем ужаснее становится синтаксис… хорошо хоть даже сами microsoft большинство этого ужаса не используют в примерах
UFO just landed and posted this here
Возможность размещать объекты в куче — означает nullable.
Создаем ссылочную переменую и забываем проинициализировать. Что там будет храниться? Т.к. это ссылка, значит null, потому что дефолтное значение ссылки — null.
Делаем вывод, что
референсные типы являются nullable по определению
Создаем ссылочную переменую и забываем проинициализировать.
Хммм. Ok.

Что там будет храниться?
Ничего, потому что программа не скомрилируется и не запустится — чем такой ответ был бы плох?

Т.к. это ссылка, значит null, потому что дефолтное значение ссылки — null.
Делаем вывод, что
А можно «мусок», как в C оставить. Или ссылку на какой-нибудь специальный синглтон.
Создаем ссылочную переменую и забываем проинициализировать. Что там будет храниться?


Есть языки в которых Вы не сможете забыть проинициализировать переменную ссылочного типа, если она явно не обявлена как nullable: Scala, Kotlin, TypeScript и C# 8 как пример, ваш код просто не скомпилируется.

На самом деле в C# нельзя забыть проинициализировать переменную даже если она nullable.

Нельзя, но если прочитать C#-спецификацию, то можно.
C# defines seven categories of variables: static variables, instance variables, array elements, value parameters, reference parameters, output parameters, and local variables.

Пруфлинк

Вы же не станете возражать, что забыть проинициализировать, например, instance variable можно? И при этом она будет автоматичестки инициализирована default-значением?
Помню, в ранних черновиках для “Nullable Reference Types” было совершенно здравое предложение просто добавить символ “!” ко всем полям, переменным и тд. которые не могут быть null. По сути, это был бы аналог атрибута [NotNull]. Это решало бы проблему с null ref там, где ее надо решить и не ломало бы обратной совместимости.
Оно не совсем здравое

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 ref types.
Идея помечтать обязательные поля "!" имеет обратную совместимость и не требует костылей в виде опций компилятора.

но если выбирать между старыми проектами и новыми, то выбор очевиден

«Детей, конечно, нельзя убивать. Но что-то с ними надо делать». Даниил Хармс.

Вы же понимаете, что «Ошибку на миллиард долларов» так просто не исправишь? Процесс будет долгим и сложным. Если включить nullable контекст во всем проекте, то программиста завалит предупреждениями, это уже обсуждалось неоднократно. К счастью, контекст можно включать для отдельных участков кода с помощью директив, это позволит постепенно перевести проект на новые рельсы, путем постепенного увеличения кода в non-nullable контексте. Всё учтено могучим ураганом.
Здесь проблема, что теперь над каждым полем, свойством, параметром надо дополнительно думать, может ли оно потенциально быть null. Раньше мы думали только об Nullable значимых типах, а теперь нужно вообще обо всех типах. Причем, эта настройка только проверяет, что вы сами в вашем коде не можете присвоить null, но есть огромное число фреймворков (тот же MVC), где объекты создаются им самим из, например, http запроса. Что должно быть, если свойство не null, но в запросе его нет — exception или все-таки значение null? Сейчас это будет null. Т.е. потенциально возможно, что даже nonnullable reference type все равно будет равен null. И теперь в каждом месте программы я должен об этом думать…

но позвольте! а как вы раньше писали код, серьезно не задумываясь о том, что, в какой то конкретный момент, вот в этом вот месте, вы планируете возвращать null, и что вызывающий код должен сделать соответсвующую проверку? или вы просто по дефолту везде ThrowIfNull писали?


а что же касается "огромного количество фреймворков", то здесь ожидается полезность фунции только если конкретный фреймворк использовал это нововведение. скорее всего, MS введет атрибут [DontCheckForNullables] для референсов разных DLL.

Задумываться над чем, простите? Может ли объект быть null? Может, т.к. это reference type. А вот сейчас мы будем смотреть, есть ли у него «знак вопроса» или нет. Но самое ужасное пока в этом новшестве то, что runtime не гарантирует, что если вы объявите свой тип без «знака вопроса», то в нем не будет null.
Т.е. вот у нас функция принимает строковый параметр (без ?). Нужно ли проверять его на null? В текущей парадигме — да, в новой — нет. Хотя null потенциально может быть и там и там.
Текущее решение — не доделано, это костыль, чтобы улучшить проверки кода на потенциальные NullReferenceException. Но оно может привести к тому, что многие начинающие или низкоквалифицированные разработчики (коих хватает) будут считать, что в таких типах null не может быть никогда и это может привести не к уменьшению, а к увеличению этих самых NullReferenceException-ов, которые как известно, мы получаем в самый неожиданный и непредсказуемый момент (иначе бы мы их выловили бы еще на стадии тестирования).

Плюс '?' и так уже используется для nullable value types: int vs int?

Для Value Types, "?" — это синтаксический сахар для структуры Nullable<>:

int? = Nullable<int>

Для Reference Types, "?" — это просто подсказка компилятору + добавляется [NullableAttribute] (подсказка компилятору при испльзовании кода из других сборок)

Разница ещё в том, что в Value Types "?" логически относится к типу, а в случае с Refernce Types к переменной, полю или методу, так, что с точки зрения логики, надо было бы писать:

string myVariable? = null

a не

string? myVariable = null

Он и относится к типу, просто это тип анонимный. То, что тип реализован атрибутом, это уже детали реализации.

Не совсем понял про анонимный тип: в примере тип переменной это string и он никакой не анонимный.

Нет, тип второй переменной — string?

string? это не анонимный тип, это обычный 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>

string? это тот же тип 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?» был другим типом, то требовалось бы приведение типов, но код ниже будет скомпилирован без предупреждений:

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. А атрибут относится не к типу, а к переменной.

По поводу Int32 и Double — это честное неявное приведение типов, которое не зависит от каких-либо предварительных проверок.

Я такое приведение могу сделать и для других типов определяя оператор «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;

надеюсь они уберут этот бесполезный функционал

А будет ли в c# аналог lateinit того же Котлина?
Ну пока не планируется. По сути, если вы инжектите свойство через DI, вам придется приписать к типу вопросительный знак.
Другое. Запрос к api возвращает json, который десериализуем в граф объектов. И хоть мы знаем, что у нас после этого будут инициализированы все свойства, приходится определять все поля nullable и потом в тысяче мест добавлять!.. Или же инициализировать свойства значениями, что в случае вложенных классов может быть очень развесисто и попросту захламляют кучу, ведь сразу после создания объекта все эти свойства будут перезаписаны.

А хотелось бы сказать компилятору «да я уверен, что там null не будет, не заставляй меня их инициализировать»

Для этого всего-то нужно инициализировать свойства в конструкторе...

А в чем отличие? Ну кроме места где будет инициализация? Результат тот же.

Если инициализировать в конструкторе есть гарантия, что в уже созданном объекте не будет null, иначе состояние, когда есть объект и там null — допустимо (в процессе десериализации)

Вы не понимаете. Хватает ситуаций «я уверен, что на момент чтения оно не будет null, поэтому не заставляй меня делать его nullable, раз уж у компилятора не хватает ума самому это вычислить». В котлин для этого есть модификатор lateinit (по названию видно что он делает). Это и различные dto без конструкторов, методы вида Init, на границе с UI тоже пригождается и на границе с жавой (там ведь тоже никакого null safety). Вот прям такая же ситуация и с шарпом — множество обычного кода, который знать не знает про nullable

Инициализация в конструкторе это костыль
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 этих данных недостаточно для работы? А с чего он взялся решать такие вопросы? А бизнес-слой считает, что ему данных хватает, но между клиентом и ним сидит чересчур умный сериализатор и не даёт данным дойти.


Это у него постусловия такие.

Постусловия можно поменять. Может вы имели в виду тех-задание, против которого нельзя возразить и ничего нельзя поменять? Ну да, с таким тех-заданием придётся выкручиваться.

Если бизнес логике хватает данных когда нет Weather.City значит соответствующее поле буде обозначено как нулейбл.
И проблемы нет

Сериализатор ничего не знает о бизнес-логике, поэтому он не в курсе, обязателен ли Weather.City или нет.

А с чего он взялся решать такие вопросы
Именно так. Конечно можно сказать, что не дело сериализатора проверять заполненность и пусть проверяет уже на месте. Но если апи четко объявляет, что такого не будет (спека), то нарушение постусловия означает, что система работает неправильно.
Допустимо ли это и пропустить дальше или упасть «fail fast» зависит от приложения. Все эти «fail fast» обычно все исчезают в процессе разработки и тестирования. И если далее такое будет, то это ну совсем нештатная ситуация, которая требует немедленного внимания, потому что логике быть не должна ни в коем случае.

И да, поле с возможностью null будет nullable (или помечено аттрибутом, раз уж #nullable не зашел)

Постусловия можно поменять
Нельзя. Если имеется код, который завязан на контракт (спеку) кода сериализации, то ничего менять уже нельзя.

зы: конечно вы правы про то, что «каждый должен заниматься своим делом». Но подход fail-fast себя оправдывает, если ошибки, которые он ловит, можно починить до релиза. Если же это сервис, который должен быть устойчивый ко всем «а что если», то там предусматриваются все варианты, а не «зови программиста»

Так как у вас на выходе "сериализатора" не DTO, а фактически частично валидированные модели с частичными гарантиями (и у вас выходит уже не совсем сериализатор, а целый API), то если вам хочется донести эти гарантии до пользователя, вам придётся делать промежуточный слой из настоящих DTO, например:


JSON -->  JObject --> new Weather(JObject dto)

И тогда Weather может спокойно сделать свои поля non-nullable, а все проверки держать в своём конструкторе (а если валидация сложная и в один конструктор не влазит, то в фабрике).

UFO just landed and posted this here

В 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.

UFO just landed and posted this here

Ну, а для ссылочных типов null есть. Фактически для них только Option был до 8.0

Во многих задачах значение null не эквивалентно отсутствию значения.

Да, но это очень сложно доносить до людей, т.к. при использовании Option появляется дополнительный уровень indirection, а синтакстса для распаковки оных, в отличии от например Swift-а, в C# еще нет.
Появление var, lambda функций, linq, nullable VALUE types, tuples, nameof, async await, $"..." — как же это все было круто! Благодаря этим и многим другим фичам C# сегодня — это, на мой взгляд, лучший язык программирования.
Но Nullable Reference Types — это какой-то перебор и шаг не туда.
Это очень странное решение, которое выливается в множество неочевидных нюансов, включая обратную совместимость с существующим кодом, отсутствие поддержки в runtime (хоть это и обещают сделать в следующих версиях, интересно как?) и многое другое. Уже сейчас, когда новой версией языка по сути еще никто не пользовался, это нововведение уже вызвало некий раскол среди разработчиков, т.к. плюсов и минусов у этого решения хватает.
Интересно, что Nullable Value Types (int?) такой реакции не вызывали, они очень органично «зашли» в язык.
Действительно, реализация получается пока-что весьма неоднозначной (не стоит забывать, правда, что оно ещё в бете, и может поменяться). Особенно смущает то, что сам по себе non-nullable reference тип никак не гарантирует, что в этой переменной не придёт в рантайме null, только из-за того, что кто-то где-то поставил один лишний '!'. Вот интересный взгляд на nullable reference с точки зрения статического анализа кода, и как он влияет на его работу.
Nullable Reference Types — то, что должно быть изначально. В котлине он изначально и там с этим очень неплохо. А вот в c#… прямо сейчас я чиню очередной NRE, причем в библиотеке, а ведь его могло бы и не быть, будь от этого защита.

и «поддержка в runtime» не нужна. Ведь основная задача этой функциональности — позволить на этапе написания кода избегать NRE с нулевой стоимостью во время выполнения.

Просто «поздно решили внедрить» и поэтому приходится делать это костыльно

Undefined behavior в C — это совсем не то. Это когда are[-1] в коде может привести к чему угодно — от вычитывания любого элемента из массива, до восстания SkyNet.


У вас же — возможно surprising behavior, но ни как не undefined.

Строго говоря вы правы, не спорю. Но результат один: придется дебажить.

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

UFO just landed and posted this here
Ну, люди пишут в соответствии с тем фактом, что им с этими фичами работать.
UFO just landed and posted this here
Дефолтные реализации интерфейсов — это, на самом деле, почти миксины. Именно для этого создавался пропоузал, и это абсолютно замечательная фича.

А вот, например, паттерматчингом я никогда не пользовался (разве что 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
Смею предположить, что вы проверяете на VS2019 RTM. А надо на Preview.
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
Значит то Preview которое у вас — не самое свежее.

Эти VS и NET Core Preview до сих пор самых свежие из доступных. Разве что ещё nightly builds какие-нибудь есть.

Это, кстати, соответствует майкрософтовской документации (см мой коммент выше), что ^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
Тут я действительно ошибся. Конечно ..2 даст тот же результат, должно было быть ..^0.

0..^0 это тоже весь массив.
Тогда уже 0..^1.

дизайнеры всех современных языков «продолбали» как минимум несколько важных аспектов [...] zero-terminated strings, когда длина строки вычисляется за О(n).
Ну, не так уж и всех, а лишь тех, кто решил не заморачиваться с нормальным строковым типом и обойтись char[]. В любом приличном современном языке (да и не только современном, вспомните тот же Паскаль) строки — один из базовых типов, который неплохо оптимизирован (во всяком случае, для более-менее коротких строк никто длину не вычисляет).
Норму по нововведениям выполнили. Мне кажется скоро весело придут к обертке над ссылочными типами которая не может быть null. CanNotBeNullable<>
Как я упомянул в посте, любой ссылочный тип — nullable by definition, так что ваше предложение не логично по сути.

В принципе, попробовал все перечисленные фичи.


Мнение:


  • 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 ― Кому-то точно это принципиально.
if (x is string s) было еще в предыдущих версиях языка.
Как же жаль, что Котлин так и не запилили под дотнет (

Родному F# трудно играть на одном поле с C#. Всё взлетает, взлетает, никак не взлетит. Последний недостаточно плох, чтобы с него дёргаться. Котлин, на сколько я его себе представляю, это что-то а-ля C# или нечто среднее между C# и F#, лишь бы забыть про Яву.


Котлин под дотнет мог бы быть способом перетащить людей с платформы Java на Net. Кому-то это надо?

Я думаю, также C# недостаточно плох по сравнению с Котлин тоже. Так что котлину придетбя бороться с C# для которого есть большая поддержка

Проблема развития старых языков, в т. ч. c# — в необходимости сохранять обратную совместимость. Поэтому новые фичи (e. g. Nullable references) выглядят как костыли, а язык превращается в монстра(полюбуйтесь на с++).

И на другие языки, например Scala, тоже были надежды что сделают под CLR. Но если это делать, придется конкурировать с C#-ом, а это практически невозможно, т.к. по сути это конкуренция с МС со всеми вытекающими.

Какими вытекающими? JetBrains сделали Rider — конкурент VS, причём успешный.
По-вашему под clr, кроме шарпа, не может быть юзабельных языков?

Юзабельные представить можно, тем более что в NET они есть. Юзабельные и не нишевые ― не очень представляю. И не очень представляю мотивацию, зачем одним упираться делать такой же, но другой язык (плюс инструментарий, без которого он сразу никому не нужен), а другим его использовать вместо существующего мейнстримового C#. Если бы последний был уродцем, которого все просто терпят до появления альтернативы, то да. А так нет.


Rider ― это ещё одна хорошая IDE для NET. [Глупая_аналогия]Открыть ещё одну конкурирующую булочную, думаю, сильно проще, чем конкурировать с самим хлебом.[/Глупая_аналогия]

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

Принципиально разная ситуация. Одна из важных (может даже самая важная) платформа для Java — это Android. Разработчики которого очень сильно не ладят с разработчиками Java. Потому использовать тот факт, что Java — «родная», а Kotlin — «неродной»… не получится.

В случае с же CLR и C# все популярные платформы контролируются Microsoft, которому продвигать что-то, кроме C# — совершенно неинтересно.

Почему неинтересно? А если это сделает разработку под их экосистему комфортнее?

А непонятно куда комфортнее. В C# итак хорошо. Плюс у МС есть ряд нишевых .NET языков в Research, это F#, C-omega, P/Zing, Q# в конце концов. Какие-то их тех идей мигрируют в C#, самый яркий пример это фичи F#-а которые постепенно появляются в C# (так глядишь и до функциональных списков дойдем).
Может, конечно может. Просто это титаническая работа которая должна давать компании, которая это реализует, некоторую отдачу. Можно ли делать это силами комьюнити — не уверен, под .NET тоже были разные попытки делать языки (например Boo и Nemerle), они достигли некой зрелости но сообщество не стало ими массово пользоваться, хотя даже JetBrains косвенно инвестировали в свое время в Nemerle (проект Nitra).
Про индексы, и прочее. Там все немного поменялось же:
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.
Поведение индексов поменялось, так что на текущий момент в RTM и Preview они работают по-разному. Ну и плюс в RTM нехватает некоторых типов, которые нужно добавить вручную. Именно поэтому я рекомендую установку Preview.
Классная статья, очень интересно читать, хотя я даже не пишу на сишарпе
Ну, возможно эта статья замотивирует вас писать на шарпе. Хотя фичи выше — не те которые должны мотивировать.
Знатоки, напишите статью, и поделитесь пожалуйста примерами до чего дошел Pattern Matching в F# или других языках развивающих эту концепцию? Хотелось бы понять «о чем мечтает команда разработчиков C#» и к чему стремится прогресс. И конкретно, а где нибудь уже дошли до разбора выражений языка средствами языка?

Отдал был часть кармы если бы Хабр позволял «краудфайндить» на статьи.
Документация конкретной реализации не дает виденья. У меня остался возможно наивный вопрос, вот эта фича развивается от релиза к релизу, так что ее двигает и куда ее двигают? Как из заявленной решаемой задачи «разложения данных на составные части или извлечения информации из данных различными способами» — например пусть это будет конкретно Natural Language Processing — вырастает запрос на pattern-matching? Никак не вырастает потому что программеры работающие с этой задачей (NLP) работают с функциями своих NLP шных библиотек и мыслят в них. Зачем им новые statementы?

Если бы проще заявили «немного полезного синтаксиса над рефлекшном» я бы не мучился, а так остается недоумение — что я упускаю.

Еще предстоит разобраться что было с головой, что так повело рассуждение. Ошибка. Спасибо что обратили внимание. Сейчас бы написал «немного полезного синтаксиса вокруг условных присваиваний».
UFO just landed and posted this here
А напишите коротенькую статью как это бы выглядело в C#? Слева «как записать сейчас», справа «было бы здорово».
UFO just landed and posted this here
У меня есть один продукт, написаный на F# с использованием активных рекурсивных паттернов. Посли полгода я не могу читать свой собственный код. И этим все сказано. Фичи действительно очень мощные, но им грош цена если они порождают write-only code.
UFO just landed and posted this here
Я пишу код который не только тяжело прочитать, но совершенно непонятно как рефакторить его в то, что потом прочитать можно.
При достижении определенного уровня композиции это общая проблема. Конструкции тут не при чем (известно с лиспа). Если проблема решается разворачиванием композиции в императивный код — это оно самое. Жаль что нет готовых инструментов для такого разворачивания (аттрибутов подсказок например).

Вопрос:


foreach (var i in 0..10) { }

Это не работает, потому что у Range отсутствует энумератор. Его ещё не сделали или его не собираются делать?


Можно спросить по-другому. Такой foreach компилятор мог бы заменить на for/do/while безо всяких энумераторов. Спецификация foreach может быть расширена на нативную поддержку Range. Компилятор ещё не умеет это делать или такой поддержки не было в планах?

Но… это ведь нелогично. Если у Range есть Enumerator, значит можно написать и что-то вроде
foreach (var i in ..)

Но код выше ↑↑↑ ничего не значит, если он не ассициирован с каким-либо контейнером. Чисто теоретически конечно можно было бы на этом месте компилировать не просто Range а какой-нибудь DefiniteRange, но для этого нужно натренировать компилятор чтобы он понимал, что ни первый ни второй аргумент Range-а не являются «с конца».
А от extension everything они отказались? Они хотели новый синтаксис добавить для расширителей и дать возможность «прививать» дополнительные поля к закрытым классам.
В 2017 году были разговоры об этой фиче, а потом что-то утихло…
Да, как-то стихли обсуждения всего этого, хотя судя по тому как вероломно прикрутили Default Interface Methods в .NET Core, могли бы и Extension Everything сделать.

Default interface impl блокирует интероп с жабой и свифтом, а у разрабов на этот интероп мега-планы (см. .NET 5). Оно добавлено не потому, что в самом языке нужно (очень многие были против), а потому что в остальных языках есть, и с ними надо нормально взаимодействовать.


А вот extension everything — по большей части синтаксический сахар, хоть и очень приятный. Не горит, скажем так.

А вот 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.

Здравствуйте, пробовал на version 16.3.0 Preview 1.0
Варианты [0..2] и [..2] дают одинаковый результат: 1, 2
Может это был баг?
Нет, на самом деле я просто ошибся, в данном случае они и должны давать одинаковый результат.
Sign up to leave a comment.

Articles