«Восьмерка» еще даже не вышла RTM а я уже пишу про нее пост. Зачем? Ну, основная идея что тот, кто предупрежден — вооружен. Так что в этом посте будет про то что известно на текущий момент, а если это все подстава, ну, поделом.



Nullable Reference Types


Я не знаю, в курсе вы или нет, но дизайнеры всех современных языков «продолбали» как минимум несколько важных аспектов. Один из основных продолбов — это неинициализированные объекты, указатель на которые имеет значение NULL в С и nullptr в современном С++. Другой аспект — это zero-terminated strings, когда длина строки вычисляется за О(n). Но мы сейчас говорим за null.


C# это конечно тоже зацепило в разных ипостасях, вот например:


public class Person
{
  public string FirstName { get; set; }
  public string LastName { get; set; }
  public string MiddleName { get; set; }
  public Person(string first, string last, string middle) =>
    (FirstName, LastName, MiddleName) = (first, last, middle);
  public string FullName =>
    $"{FirstName} {MiddleName[0]} {LastName}";
}

Пример выше очень хорошо иллюстрирует проблему. У человека может быть отчество но у меня, например, в паспорте его нет и следовательно непонятно что писать в это поле. Дефолтное значение строки в C# это null. Если интерполировать это значение в пустую строку ($ за кулисами делает string.Format()), ничего страшного не будет. Но если вы попробуете получить первую букву null-значения, вы получите исключение NRE (NullReferenceException).


Кардинального решения этой проблемы нет, т.к. если внезапно запретить null, придется написать 100500 статических проверок на инициализацию всех объектов во всех брэнчах конструктора. Или перейти полностью на Optional типы что, собственно, тоже не совсем решает проблему.


Системы статического анализа вроде Решарпера уже давно пытаются как-то облегчить участь разработчика, предупреждая о возможных косяках. Собственно для этого придумали аннотации (NuGet пакет JetBrains.Annotations) которые можно добавить в проект вот таким вот образом:


public class Person
{
  public string FirstName { get; set; }
  public string LastName { get; set; }
  [CanBeNull] public string MiddleName { get; set; }
  public Person(string first, string last, [CanBeNull] string middle) =>
    (FirstName, LastName, MiddleName) = (first, last, middle);
  public string FullName =>
    $"{FirstName} {MiddleName[0]} {LastName}";
}

Код выше заставляет системы статического анализа ругаться на возможный NRE в точке MiddleName[0].




Но Microsoft… как всегда берет то, что делает другие и банально копирует. Вообще есть шутка, что в долгосрочной перспективе VS просто скопирует все фичи Решарпера. А потом запретит плагины. Поэтому хорошо что есть райдер.


Короче, МС естественно не стали менять язык. Точнее как не стали, они конечно его поменяли. Вместо решарперных аннотаций, в C#8 можно написать вот так:


#nullable enable

Просто написав от эту штуку наверху файла вы меняете поведения компилятора. Теперь, при написании чего-то вроде


var p = new Person("Dmitri", "Nesteruk", null);

вы получите следующий warning:

1>NullableReferenceTypes.cs(26,48,26,52): warning CS8625: Cannot convert null literal to non-nullable reference or unconstrained type parameter.

Да-да, просто предупреждение, а не ошибку (хотя treat warnings as errors никто не отменял).


Ну да ладно, а что дальше? А дальше нам хочется как-то все-таки сказать C#, что теоретически MiddleName таки может быть null поэтому доступ с индексом [0] тоже нужно проверять.


Для этого мы меняем поле на вот такое:


public string? MiddleName;

Хмм, что это? Кому-то может показаться что string? эквивалентно Nullable<T>, но напомню что в этом типе, T : struct, так что очевидно это что-то другое. Этот вопросик — это всего лишь подсказка компилятору, т.к. по факту тип поля все еще обычный string.


Теперь компилятор выдаст вам еще один варнинг на доступ по индексу ноль. И чтобы оно заработало вам придется как-то перестраховаться, например написав:


public string FullName =>  $"{FirstName} {MiddleName?[0]} {LastName}";

Теперь самый важный вопрос: что же поменялось в IL? С точки зрения исполняемого кода — ничего! Но с точки зрения метаданных поменялось конечно: теперь в типе который использует nullable аннотации все типы которые являются nullable проаннотированы атрибутом [Nullable]. Сделано это по понятным причинам: чтобы потребители вашего кода могли использовать ваши аннотации.


Чувствительность к проверкам


Аннотации в C#8 работают в какой-то мере как Котлиновские смарт-касты. Иначе говоря, если у меня есть апи который выдает nullable-тип


string? s = GetString();

То конечно при попытке достучаться до первой буквы я получу warning. Но если я напишу так:


if (s != null){  char c = s[0];}

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


Предотвращение лишних проверок


Есть два способа отключить проверки на null. Первый — это просто писать код без «вопросиков», тем самым констатировав тот факт что все твои поля, параметры и так далее вообще ни при каких условиях null-ами быть не могут.


Второй подход — это явно сказать компилятору что в этой точке проверка не нужна. Вот несколько примеров:


  • (null as Person).FullName конечно выдаст нам warning

  • (null as Person)!.FullName warning уже не выдаст, т.к. мы явно просим не проверять выражение

  • (null as Person)!!!!!!!!!!!!!.FullName тоже является валидным выражением и тоже отключает проверки

  • (null as Person)!?.FullName тоже валидно и все же делает в этом случае проверку на null; примечательно что обратное использование, ?!, не скомпилируется


Проверки в либах


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


Type t = Type.GetType("abracadabra");Console.WriteLine(t.Name);

и не получить никакого предупреждения. Я-то знаю что Type.GetType() возвращает null когда даешь невалидное название типа, но поскольку BCL пока еще не размечена nullable аннотациями, компилятор это съедает.


И нет, мы не можем «форсировать» подобные проверки кодом вроде


Type t = Type.GetType("abracadabra");
Type? u = t;
Console.WriteLine(u.Name);

Код выше все равно не выдаст предупреждение. Очевидно, компилятор считает что t != null, следовательно u тоже не может быть равно null, сколько его не декорируй.


Итого


Nullable reference types — сомнительной полезности фича, которую еще рано использовать. Оригинальности в ней мало. В долгосрочной перспективе она, конечно, должна помочь нам как-то бороться с рисками nullability но глобально она проблему, как вы понимаете, не решает.


Индексы и Диапазоны


На матстатистке есть такое хороше упражнение: брать диапазон чисел, обладающих тем или иным распределением, преобразовывать его, и потом считать статистику по результату. Например, если X~N(0,1), чему равны E[X?] и V[X?]?


Диапазоны это очень хорошо, но дизайнерами сишарпа явно хотелось получить диапазоны в стиле Питона, а для этого пришлось ввести понятие «с конца».


Итак, у нас за кулисами появляются два новых типа: Index и Range.


Index


Вы никогда не задумывались, почему это индекс в массив обязательно int а не uint? Все просто: этот ваш пресловутый индекс это просто отступ от указателя на начало массива (привет Си). Поэтому он теоретически может быть отрицательным, хотя конечно в C# запись x[-1] лишена всякого смысла.


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


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


Index i0 = 2; // implicit conversion

Индекс выше, как вы понимаете, будет ссылаться на третий элемент с начала той или иной коллекции.


У самого типа Index есть два свойства (зачем сделали свойства а не поля — тот еще вопрос):


  • Value, то есть сколько элементов нужно отсчитать

  • IsFromEnd — булевое значение, показывающее, нужно ли отсчитывать от конца коллекции а не от начала


Структуру можно инициализировать просто вызвав конструктор:


Index i1 = new Index(0, false);

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


var i2 = ^0; // Index(0, true)

Опаньки! Многие из вас наверное хотели, чтобы оператор ^ сделали возведением в степень (мне как математику эта идея комфортнее, LaTeX, все такое), но по факту это теперь специальный синтаксис для создания индекса «с конца».


Встроенные типы, такие как массивы или строки, конечно же поддерживают индексер (operator this[]) для типа Index, так что смело можно писать


var items = new[] { 1, 2, 3, 4, 5 };
items[^2] = 33; // 1, 2, 33, 4, 5

Range


Следующий кусочек этого паззла — это тип Range, который представляет из себя линейный, направленный строго по возрастанию диапазон индексов, строго с шагом 1. Для него тоже введен новый синтаксис:


X..Y

Что означает «все элементы от X включительно до Y». При этом, включает ли диапазон значение с индексом Y или нет — нельзя сказать не посмотрев на Y. Об этом очень скоро.


Итак, вот несколько примеров:


  • var a = i1..i2; // Range(i1, i2) — полноценный диапазон с началом и концом

  • var b = i1..; // Range(i1, new Index(0, true)); — диапазон от i1 и до конечного элемента

  • var c = ..i2; // Range(new Index(0, false), i2) — диапазон от самого первого элемента и до индекса i2

  • var e = ..; — вообще весь диапазон, то есть от первого и до последнего элемента

  • Range.ToEnd(2); — эта и подобные статические функции — как раз то, что использует компилятор за кулисами; все это можно лицезреть, если открыть сборку в dotPeek, ilSpy или другом декомпиляторе


Включается ли конечный элемент?


Окей, знаете что в С++ толстым слоем разбросано неопределенное поведение (undefined behavior?). Ну так вот, в реализации Range в C# тоже затаился сюрприз. Суть примерно в следующем: последний элемент включается только если он взят «с конца».


Представьте массив x = {1, 2, 3}. Если взять x[0..2] вы получите {1, 2}, а вот если взять x[..2] или например x[..], вы получите {1, 2, 3}.


Это немного выносит мозг т.к. элементы x[2] и x[^1] — это одинаковые элементы, но семантика их поведения как часть Range-а разная!


Еще немного семантики


Во-первых, диапазон X..Y будет работать только если X <= Y (надеюсь вы включили лигатуры?). Если же вы решили сделать нисходящий диапазон (например 7..3), то вы просто получите ArgumentOutOfRangeException. Это очень неприятно т.к. часто хочется получить индексы массивы как некоторые выходные значения из функций, и проверять что диапазон восходящий ну совсем не хочется.


Во-вторых, «шаг» не включен в спеку, то есть нельзя написать 1..2..100 и получить только нечетные числа. А было бы удобно. Эта фича реализована в языках вроде MATLAB.


Range ведет себя в стандартных типах вот так:


  • В массивах, он дает копию подмассива, прям копируя каждый элемент

  • В строках происходит вызов Substring(). Строки в C# иммутабельные так что создается новая строка.

  • На коллекциях можно вызывать AsSpan() передавая ему Range.

  • В Span тоже можно засунуть RangeSpan.Slice(), получив под-диапазон.


Конечно, вы можете также запилить поддержку Index/Range в своих собственных типах с помощью переопределения operator this[]. При этом нужно понимать, что не во всех коллекциях понятие «с конца» работает хорошо. Например, если у вас односвязный список (singly linked list), то брать что-то «с конца» — плохая идея.


Итого


Полезные в целом фичи, которые являются просто слоем синтаксического сахара который компилятор разворачивает в инициализацию разных struct-ов. Включительность-исключительность диапазонов в зависимости от того, «хвостовой» ли закрывающий индекс понравится не всем, как и невозможность обходить коллекции задом наперед.


И да, решарпероводам будет много всяких веселых инспекций:




Default Interface Members


Никто не рискует породить столько ненависти сколько реализация дефолтных интерфейс мемберов, то есть бархатное преврашение интрефейсов в абстрактные классы. И первый вопрос, на который нужно ответить — зачем?


Ну типичная мотивация такая. Вот допустим вы хотите сделать Enumerable.Count() — его конечно можно делать через while (x.MoveNext()) но это немного бредово для коллекций, чья длина известна заранее. Поэтому мы втыкаем в Count() проверки типа:


if (x is IList<T> list)
  return list.Count;

Логично? А как насчет IReadOnlyList<T>, для него такая оптимизация будет? Что значит «не сделали»?!?


Ситуация, приведенная выше наводит нас на вынужденное, очень грубое нарушение open-closed principle да и других принципов SOLID, т.к. делать проверки на все возможные типы ради оптимизации — это провальная затея.


А что можно сделать? Ну, можно было бы, чисто теоретически, как-то взять и добавить реализацию Count(), специфичную для IReadOnlyList<T>, прямо в интерфейс, возможно как метод расширения. Только это тоже не сработает! Откуда мы знаем что экстеншн-метод IReadOnlyList<T>.Count() является перегрузкой метода IEnumerable<T>.Count()? Правильно, не знаем! Нужен другой подход.


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


Тонкости использования


Но давайте для начала посмотрим на более приземленный пример:


public interface IHuman
{
  string Name { get; set; }
    
  public void SayHello() 
  {
    Console.WriteLine($"Hello, I am {Name}");
  }
}

public class Human : IHuman
{
  public string Name { get;set; }
}

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


Human human = new Human() { Name = "John" };
human.SayHello(); // will not compile

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


Почему, спросите вы? Потому, что вся соль этих методов в том, чтобы добавлять их пост фактум, когда вашим интерфейсом уже пользуются. А что если за это время класс Human обзавелся собственным SayHello()? Правильно, будет конфликт.


Поэтому дизайнеры приняли такое решение: дефолтные методы доступны только через сам интерфейс, то есть требуется явное или неявное приведение типа к интерфейсу:


IHuman human = new Human() { Name = "John" };
human.SayHello();
((IHuman)new Human { … }).SayHello();

Наследование интерфейсов


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


public interface IHuman
{
  string Name { get; set; }
  
  void SayHello() 
  {
    Console.WriteLine($"Hello, I am {Name}");
  }
}
public interface IFriendlyHuman : IHuman
{
  void SayHello()
  {
    Console.WriteLine(      $"Greeting, my name is {Name}");
  }
}
((IHuman)new Human()).SayHello();
// Hello, I am John
((IFriendlyHuman)new Human()).SayHello();
// Greeting, my name is John

Заметьте, что в коде выше IFriendlyHuman.SayHello() как бы должен override-ить IHuman.SayHello(), но этого не происходит! Что же нужно сделать чтобы вызов SayHello() стал действительно виртуальным? Нужно явно сказать об этом:


public interface IFriendlyHuman : IHuman
{
  void IHuman.SayHello()
  //   ^^^^^^
  {
    Console.WriteLine(      $"Greeting, my name is {Name}");
  }
}

Вот в этом случае вызов SayHello() на любом интерфейсе, будь то IHuman или IFriendlyHuman будет виртуальным и уже не важно, к какому из них вы скастовались:


((IHuman)new Human()).SayHello();
Greeting, my name is John
((IFriendlyHuman)new Human()).SayHello();
Greeting, my name is John

Diamond Inheritance


Естественно, в ситуации когда вы можете иметь два «честных override-а» в двух интерфейсах-наследниках породит конфликт в случае, если вы попытаетесь реализовать их оба:


interface ITalk { void Greet(); }
interface IAmBritish : ITalk
{
  void ITalk.Greet() => WriteLine("Good day!");
}
interface IAmAmerican : ITalk
{
  void ITalk.Greet() => WriteLine("Howdy!");
}
class DualNational : IAmBritish, IAmAmerican {}
// Error CS8705 Interface member 'ITalk.Greet()' does not have a most specific implementation. Neither 'IAmBritish.ITalk.Greet()', nor 'IAmAmerican.ITalk.Greet()' are most specific.

Проблема тут в том, что компилятор не может найти «более специфичный» (то есть, ниже в иерархии наследования) интерфейс для использования и, в результате равнозначности, не поймет что нужно делать если кто-то вызовет какой-нибудь IAmAmerican.Greet() — ведь по идее нужно гулять по виртуальной таблице, а куда идти-то, если варианта два?


Итого


Фича для писателей АПИ. Обычным пользователям скорее всего не стоит беспокоиться, особенно если вы, как я, контролируете весь свой код и вам не страшно в любой момент менять его API. Единственный реальный кейс — это когда вот прям нужно оверрайдить экстеншн-методы. У вас есть подобные юз-кейсы?


Pattern Matching


Эту фичу нагло крадут из F# уже на протяжении нескольких минорных релизов. Конечно — в F# ведь эта фича очень удобна, но она идет рука об руку с теми фичами, которых в С# нет, а именно алгебраические типы и функциональные списки.


Но несмотря на это, аналогом F#ного match стал С#ный switch. В C#8 все это обрастает дополнительными возможностями.


Property Matching


Если у объекта есть поля или свойства, можно мэтчить по ним:


struct PhoneNumber{
  public int Code, Number;
}
var phoneNumber = new PhoneNumber();
var origin = phoneNumber switch {
  { Number: 112 } => "Emergency",
  { Code: 44 } => "UK"
};

Код выше анализирует структуру объекта phoneNumber, проверяя его поля на конкретные значения. Заметьте что у нас не switch statement а switch expression, то есть выражение которое делает возврат лямбда-образным синтаксисом.


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




Ну и системы статического анализа кода тоже в долгу не останутся:




Загадка


Поскольку мы можем между фигурных скобок анализировать все что угодно, мы можем сделать и кейс, где фигурные скобки пустые:


var origin = phoneNumber switch {
  { Number: 112 } => "Emergency",
  { Code: 44 } => "UK",
  { } => "Indeterminate",
  _ => "Missing"
};

Что это значит? Да то, что теперь есть 2 кейса: _ (подчеркивание), которое словит абсолютно любой результат, и {} которое отловит любой аргумент который не null.


Пример выше определенно покрывает все кейсы, так что исключение мы на нем не словим. А что насчет вот такого?


var origin = phoneNumber switch {
  { Number: 112 } => "Emergency",
  { Code: 44 } => "UK",
  { } => "Unknown"
};

Покрывает ли пример выше все варианты? Если phoneNumber это struct — то конечно да, но представим что это class. С одной стороны кто-то может сказать что да, не покрыли ситуацию с null, но… ведь есть nullable reference types!


Вот и получается, что покрытие всех вариантов зависит не только от типа объекта но еще и от контекста компилятора.


Рекурсивные паттерны


Все-таки с неймингом у МС получается ооочень плохо, прям фееричный булшит. Сначала nullable reference types — масло масляное, ибо референсные типы являются nullable по определению, но термин recursive patterns — это еще на порядок хуже.


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


Если коротко, «рекурсивные паттерны» в C# это возможность углубиться внутри структуры того или иного объекта и проверить еще и поля-полей, если так можно выразиться:


var personsOrigin = person switch {
  { Name: "Dmitri" } => "Russia",
  { PhoneNumber: { Code: 46 } } => "Sweden",
  { Name: var name } => $"No idea where {name} lives"
};

В примере выше проиллюстрированы сразу две идеи. Первая — это то, что можно углубиться в объект person.PhoneNumber и промэтчить его поле Code на значение 46. Вторая — это то что вместо мэтчинга можно задекларировать переменную и в нее записать найденное значение чтобы им с последствии пользоваться. Это тоже иногда бывает удобно, и конечно же работает в контексте этой надуманной «рекурсивности».


Валидация


Хорошее применение всей этой кухне: комплесная валидация разных аспектов одного сложного объекта в одном switch-е. Вот например:


var error = person switch {
  null => "Object missing",
  { PhoneNumber: null } => "Phone number missing entirely",
  { PhoneNumber: { Number: 0 } } => "Actual number missing",
  { PhoneNumber: { Code: var code } } when code < 0 => "WTF?",
  { } => null // no error
};
if (error != null)
  throw new ArgumentException(error);

Как видите, в коде выше идет несколько проверок: простые проверки с паттернами, плюс ключевое слово when для сложных сценариев когда идёт не сравнение с одним значением, а нечто более хитрое.


Интеграция с проверками на типы


Проперти-паттерны можно «поженить» с проверками на типы которые появились одним сишарпом ранее. Теперь можно проверить на тип, а потом еще и распаковать структуру:


IEnumerable<int> GetMainOfficeNumbers()
{
  foreach (var pn in numbers)
  {
    if (pn is ExtendedPhoneNumber { Office: "main" })
      yield return pn.Number;
  }
}

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


Деконструкция


Надеюсь вы не забыли про деконструкцию — фичу языка которая позволяет, по сути, распаковывать тип в кортеж. Для этого тип должен реализовывать метод Deconstruct() который просто выгружает содержимое типа в out-параметры.


Так вот, эту фичу тоже можно поженить с паттерн-мэтчингом и получить следующее:


var type = shape switch
{
  Rectangle((0, 0), 0, 0) => "Point at origin",
  Circle((0, 0), _) => "Circle at origin",
  Rectangle(_, var w, var h) when w == h => "Square",
  Rectangle((var x, var y), var w, var h) =>
    $"A {w}?{h} rectangle at ({x},{y})",
  _ => "something else"
};

Код выше распаковывает содержимое прямоугольника или круга в кортежные структуры, при этом есть варианты: либо заниматься паттерн-мэтчингом, либо просто деструктурировать объекты в переменные, как показано в последнем примере. Заметьте что этот процесс тоже является «рекурсивным» — у прямоугольника есть точка начала, которая деструктурируется в круглых скобках. Прочерк (_) используется для тех аспектов деструктуризации, которые нам не интересны.


Итого


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


Заключение


Что, я не все фичи показал? Ну, я и не обещал, вообщем-то. Для начала хватит. Пока язык не релизнули я могу еще чуток поисследовать. А если и вы хотите поисследовать, вам потребуется VS 2019 Preview (да-да, preview версия а не RTM), .NET Core 3 (многое из описанного выше попросту не поддерживается в .NET Framework), ну и dotPeek тоже будет полезен чтобы понять, что же там за кулисами.


У меня пока все. Продолжение (возможно) следует. ¦

Комментарии (185)


  1. Feihoa
    02.06.2019 01:15
    +2

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


    1. Source
      02.06.2019 02:56

      Да фичи то ещё ладно, а вот синтаксис для них они выбирают какой-то слишком странный.
      В MiddleName?[0] знак вопроса ни к селу, ни к городу, семантики никакой. Сделали бы просто, что для переменной Nullable-типа вызов любого метода возвращает null обёрнутый в Nullable-тип возврата, если в самой переменной был null.
      ^0 как автор в статье написал, лучше бы для возведения в степень использовали.
      А тут можно было бы ~0


      термин recursive patterns — это еще на порядок хуже

      Зачем для этого вообще отдельный термин? Это же самый обычный паттерн-матчинг с нормальной деконструкцией и биндингом сматченных значений в переменные.
      Можно было бы обозвать "Pattern matching restrictions have been eased."


      1. mentin
        02.06.2019 05:11

        Я думаю с операторами просто — унарный оператор `~` уже есть. Так что использование ~0 создало бы неоднозначность в грамматике — то ли это индексирование с конца, то ли побитное инвертирование, как раньше. Поэтому добавили унарный оператор ^. Он, кстати, никак не мешает добавить позже бинарный оператор ^ для возведения в степень.

        Знак вопроса уже используется в подобных местах, скажем x ?? -1, возвращает х, если тот не пустой, или -1. Так же логично сделали x?.foo, x?[0]. Менять поведение по умолчанию и семантику существующего кода не хотят, ввели новые операторы.


        1. v0s
          02.06.2019 13:12
          +1

          Ну кстати ~0 для интов как раз отлично бы работал %)


          Флип всех битов как раз даёт ~0 == -1, ~1 == -2 и т.д., можно было бы сделать отрицательные индексы для "с конца", таким образом сохранить соместимость с питонистами, а остальным объяснить что надо ~0 юзать


          1. DistortNeo
            02.06.2019 13:59

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


            1. v0s
              02.06.2019 14:27

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


              1. Jesting
                02.06.2019 17:17

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


                1. v0s
                  02.06.2019 17:18

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


        1. Source
          02.06.2019 14:12

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


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


          1. ad1Dima
            03.06.2019 06:07
            +1

            ?.. Присутствует и в других языках программирования и делает то же самое


      1. yarosroman
        02.06.2019 06:30

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


        1. Prototik
          02.06.2019 21:19

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


          1. Acuna
            03.06.2019 03:44

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


            1. Prototik
              03.06.2019 04:04

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


        1. Acuna
          03.06.2019 03:38

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


          1. Prototik
            03.06.2019 04:09

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


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


          1. ad1Dima
            03.06.2019 06:12

            Потому что в андроиде ооочень старая Java. А Котлин — хороший современный язык.


            1. Acuna
              04.06.2019 21:21

              Ну я думаю язык надо выбирать в плане синтаксиса, а не новизны.


              1. ad1Dima
                05.06.2019 07:08

                и андроидовская джава очень многословна и не отвечает требованиям к современным языкам.


      1. NeoCode
        02.06.2019 10:38

        В D вроде бы используется arr[i] для индексирования с начала и arr[$-i] для индексирования с конца. То есть внутри контекста скобок доступна длина массива в виде символа $.


      1. DistortNeo
        02.06.2019 11:29
        +1

        А тут можно было бы ~0

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


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

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


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


        1. KvanTTT
          02.06.2019 16:00

          С опретором ** тоже могут быть проблемы: Proposal: Exponentiation operator.


          1. DistortNeo
            02.06.2019 17:52

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


      1. alex_zzzz
        02.06.2019 17:08

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

        Как минимум по бюрократическим причинам. В рамках C# 8.0 ведётся работа над некоторой подфичей Pattern Matching. Чтобы при разговорах о ней не размахивать руками в воздухе или не ссылаться на неё по номеру (Feature #5982), необходимо было придумать какое-то человеческое название. Конечному пользователю языка, если он на github.com/dotnet/csharplang не ходит и спецификации не читает, это название ненужно.


        1. Source
          02.06.2019 21:36

          Но название фичи ведь должно кратко описывать её суть, а не просто из случайных слов состоять? А тут в чём рекурсивность? Тут скорее "Nested deconstruction"


          1. alex_zzzz
            02.06.2019 22:40

            Можно написать паттерн внутри паттерна внутри паттерна внутри паттерна… Выглядит, правда, адски, как ни форматируй.


            Можно назвать 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);
            }


          1. ApeCoder
            03.06.2019 09:49

            А тут в чём рекурсивность?

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


            1. Source
              03.06.2019 20:22
              -1

              Рекурсивность появится если паттерн будет состоять из самого себя, а не просто из набора других паттернов.


              Если бы было так:


              var person = new Person
              {
                  Name = new FullName
                  {
                      First = "Vasya",
                      Last = "Pupkin",
                      X = "secret",
                  },
                  Age = 42,
              };
              
              if (person is { X: var x})
              {
                  Console.WriteLine(x);
              }

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


  1. mentin
    02.06.2019 01:15
    +1

    По-моему либо тут, либо в предварительной документации Майкрософта что менее вероятно, некоторая путаница насчет индексов с конца. Сам не проверял, но вот что говорит Майкрософт, в предложении:
    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 не включается, иначе бы бросалось исключение.


    1. Eldhenn
      02.06.2019 07:06

      Получается какой-то substr. 0..4 тут 4 элемента от нулевого. Да нет, зреет получается. 4..5 выберет один элемент? Они упрлс?


      1. mentin
        02.06.2019 09:08

        Не, это как раз нормальное и ожидаемое поведение в С-подобных языках.
        Вот если бы было как описал автор, то последнее значение в Range не включено, если индекс с начала, то включено, если индекс с конца — я бы решил что что-то не так.


        1. Eldhenn
          02.06.2019 09:18
          -1

          Что нормального в том, что в 4..5 помещается один элемент? Как это читать тогда вообще?


          1. mayorovp
            02.06.2019 10:34
            +1

            Все нормально и очевидно: 5-4=1.


            1. NeoCode
              02.06.2019 11:08

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


              1. khim
                02.06.2019 12:57
                +1

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

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

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


              1. AgentFire
                02.06.2019 16:07

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


                1. khim
                  02.06.2019 16:41

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

                  Вот всё как вы хотели: и все четыре варианта предусмотрены и да — таки круглые и квардартные скобки. Но «новое поколение» хочет, видимо, лично понаступать на те же грабли. Языки, созданные до их рождения, видимо «неправильные» — наверное за полвека что-то кардинально новое в математике случилось…


            1. Eldhenn
              02.06.2019 16:59

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


              1. khim
                02.06.2019 17:17
                +2

                В чём смысл такой записи?
                Вы в каждой строчке программы хотите увидеть «смысл жизни»? Или как?

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

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

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

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


                1. Eldhenn
                  02.06.2019 17:39

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


              1. Bronx
                02.06.2019 22:53
                +4

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

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


                1. khim
                  03.06.2019 00:55

                  Я думаю что претензии к тому, что a..b выглядит симметричной при том, что a входит в интервал, а b — не входит. Что, действительно, выглядит немного странно: конструкция вроде симметрична, а на самом деле — нет.

                  Но это уже чисто вопрос привычки: получинтервалы реально удобнее, а что немного непривычно выглядят — так тут уж «как получилось».


                  1. Bronx
                    03.06.2019 03:10

                    Возможно так. Хотя взять ту же пару std::begin(), std::end() — вроде выглядит симметрично, на на деле нет. Тех, кто интервалов касался, эта кажущаяся симметрия вряд ли введёт в заблуждение.


                  1. DistortNeo
                    03.06.2019 09:16

                    Как тут, симметрично?


                    for (int i = a; i < b; i++)


                    1. khim
                      03.06.2019 15:05

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


  1. hrenvam
    02.06.2019 01:29

    Я думаю увидев такой код на продакшене:

    var type = shape switch
    {
      Rectangle((0, 0), 0, 0) => "Point at origin",
      Circle((0, 0), _) => "Circle at origin",
      Rectangle(_, var w, var h) when w == h => "Square",
      Rectangle((var x, var y), var w, var h) =>
        $"A {w}?{h} rectangle at ({x},{y})",
      _ => "something else"
    };
    

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

    А вот дефолтную реализацию методов я поддерживаю. Мне подобные плюшки и в Java нравятся.


    1. math_coder
      02.06.2019 02:40
      +2

      Нормальный функциональный код.


      1. hrenvam
        02.06.2019 13:49
        -1

        Да мне как бы все равно как вы его называете.
        Вот только легко читаемым он от этого не становится!

        И еще меня умиляют такие слова как «нормальный». Их как понимать?
        Как нормальный для вас? Или среднестатистичекий по больнице?
        И если второе, так я скажу, что в аутсорсе переодически и не такая дрянь встречается.
        Но это же не означает, что подобный код перестает быть Г.
        Отнюдь, просто г-код становится чем-то приемлимым в сфере программирования.
        Вот, например, вы пишете, что для вас это «нормально».


        1. DistortNeo
          02.06.2019 14:01

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


          1. hrenvam
            02.06.2019 23:35
            -3

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

            И еще, наверное из-за отсутствия опыта или по какой-то другой неосмотрительности, до вас до сих пор еще не дошло, что ОСНОВУ ВСЕХ ИННОВАЦИЙ как раз и закладывают всякие СЕДОВЛАСЫЕ ДЯДЬКИ ЗА 50.
            Именно они стоят у руля лабораторий, которые дают вам технологие, на которые вы молитесь. Например Реймонд Ваган Дамадьян, изобретатель МРТ, ему сейчас около 80 лет.

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


            1. khim
              03.06.2019 01:03

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

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

              И это конечно факт прискорбный, что «истина недоступна большинству», но ожидаемо — так что по барабану.
              Как раз большинству она доступна. А вам — похоже, что нет.


              1. hrenvam
                03.06.2019 03:05
                -1

                Проигнорировав откровенный бред, я отвечу по сути:

                > Когда он свою первую статью про МРТ публиковал — ему 35 было

                Это была отлаженная и реально работающая технология?
                Это уже можно было называть словом «инновация»?
                И не забываем (не забываем ведь?), что мы говорим о возрасте, в котором начинается «сопротивлению всему новому»!

                Но даже, если опираться на ваши факты, то если мне 35 лет, то этого уже достаточно, чтобы формулировка: «Стареете, хуже воспринимаете нововведения — это нормально.», начала звучать по-идиотски? Т.е. 35 — это еще недостаточно «старый», чтобы уже противиться инновациям?

                Так что да… автор сообщения очень далек от истины:
                1) Потому что как правило именно за 50. Исключение составляют более «молодые» технические отрасли, и то срок снижается не сильно ну максимум лет на 10. Сколько было Маску когда тесла только вышла? 40?
                А сколько Торвальдсу когда линукс стал мейнстримом и реально вошел в жизнь каждого? А сколько им сейчас? Около 50? И эти люди ПРОДОЛЖАЮТ СВОЕ ДЕЛО, и каждый день придумавают что-то новое!
                Назовите юное 20-летнее дарование, затащившее инновацию?
                BolgenOS не считается ))))))))

                Я надеюсь не надо объяснять, что чтобы быть в 50 «Илоном», надо уже начать идти этой дорогой с юности?
                Возможно когда-либо в будущем срок «становления юного инноватора» весьма сократится и вообще человечество поумнеет, но это уже другая история.
                А на сегодняшний день в 20 лет — это не инновации, это ваши влажные фантазии. Так что да я считаю что ВОЗРАСТ — НЕ ПОКАЗАТЕЛЬ! И я привел парочку примеров в подтверждение (помните чего? именно неверности «Стареете, хуже воспринимаете нововведения»). И дело не в возрасте, а в человеке.
                Если вы утверждаете обратное, то просто, избавьте меня, пожалуйста, от дальнейшего обсуждения этого пункта (в этом случае оно никому не нужно, по-крайней мере мне).

                2) Потому что он утверждает, что знает, что именно определяет мое мнение. И это даже не зная моего возраста, а ведь именно на этом основаны его тезисы.
                Я прекрасно понимаю, что его формулировка, это не что иное как обыкновенный популизм (я не завсегдатый хабра, просто проходил мимо, но приблизительно представляю как ваша г-карма или как там ее работатет). И пипл хавает… Так вот нет… она недоступна.

                И Аристотель, и многие ученые мужи современности, и собственно лично я, считаем именно так. Вы конечно можете думать как вам вздумается. Но на этой двойственности мнений мы и закончим с этим пунктом.


                1. khim
                  03.06.2019 03:25

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

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

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

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


                  1. hrenvam
                    03.06.2019 04:06
                    -1

                    было ему тогда, оп-па, таки 36.

                    не полезем в историю комитов, не будем оценивать его вклад в «инновации» на сегодняшний день, а наконец ответим на мой вопрос

                    то если мне 35 лет, то этого уже достаточно, чтобы формулировка: «Стареете, хуже воспринимаете нововведения — это нормально.», начала звучать по-идиотски? Т.е. 35 — это еще недостаточно «старый», чтобы уже противиться инновациям?

                    как человек в 20-30-40 лет что-то сделал — что захватило мир гораздо позже.

                    в том виде в котором оно было 20 лет назад? конечно нет
                    но мы ведь не об этом да? зачем вы заставляете меня напоминать?

                    Нет, он просто шутит.

                    Точный текст:
                    «Стареете, хуже воспринимаете нововведения — это нормально.»

                    Где смеяться? Но допустим это просто неудачная шутка.
                    Но тогда спросим автора, а почему он хотя бы смайл какой-нибудь самый захудалый не вставил? Непогрешимая уверенность в собственном опезденительном чувстве юмора.
                    Да и не думаю что это шутка… но даже если…
                    А как насчет «в каждой шутке есть доля шутки»?
                    Или вы признаете, что пост автора исключительно «веселье», и не несет сколь-нибудь истинной и полезной информации?

                    заставляет хлопнуть себя ладонью по лбу и громко расхохотаться.

                    можете приступать, но хлопайте не сильно… пригодится ведь еще.


        1. math_coder
          03.06.2019 14:27

          Но это же не означает, что подобный код перестает быть Г.

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


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


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


      1. Karl_Marx
        02.06.2019 17:25

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


        1. ApeCoder
          02.06.2019 18:22
          +7

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


          1. hrenvam
            02.06.2019 23:14

            Нет он именно не читаем!
            И именно это я и хотел сказать в своих постах.

            А то, что мое мнение не разделяет большинство, то мне как бы все равно. НО! Это фактически подтверждение того, насколько некачественный код продуцирует большая часть разработчиков (причина? хз… вкатившиеся?).

            Вот типичный пример кода из статьи:

            var error = person switch {
              null => "Object missing",
              { PhoneNumber: null } => "Phone number missing entirely",
              { PhoneNumber: { Number: 0 } } => "Actual number missing",
              { PhoneNumber: { Code: var code } } when code < 0 => "WTF?",
              { } => null // no error
            };
            if (error != null)
              throw new ArgumentException(error);
            


            А написать его нужно было бы как-то так:

            if (person == null)
                throw new ArgumentNullException(nameof(person), "Object missing");
            
            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?");
            


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

            Так какая выгода от такого матчинга? Один вред.


            1. eabrega
              03.06.2019 02:04

              — этот код легко читается
              С чего вы взяли? Кто вам сказал что он легко понимается всей командой разработчиков?

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

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

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

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

              Внезапно у вас поменялась структура объекта, который надо валидировать, и вы радостно переписываете все свои if'ы.


              1. hrenvam
                03.06.2019 03:41
                -1

                > С чего вы взяли? Кто вам сказал что он легко понимается всей командой разработчиков?

                может потому что она у меня есть?

                > Реализация функционального подхода наверное.

                если вам так будет легче, то в рассматрвиаемом примере кода, реализация функционального подхода — вред для читаемости и сопровождения.

                > Нужно набирать тонну говно-if'ов

                ответите? что прям г-ифов?

                Я не знаю насколько часто вам приходилось работать с огромным количеством кода, но я вам точно скажу что для того изучить пару сотен тысяч строк кода за 1-2 дня нужно чтобы код был читаемым.

                А что такое читаемость я сейчас расскажу:

                Есть две строки…

                "3f5efded87f3e54760f46bfd6444c4ec06659e970c9039f32f00bf6dc6d9d"
                "778959537cf5c516021f162723807928944a7dc54cc1b4ec06644c9cffb24"
                

                В них нужно бегло глазами найти значение 4ec06.
                Вот и попробуйте сделать это и заодно сказать есть ли такой текст в обеих этих строчках.

                А теперь изменим эти строки на вот такие (можем менять — это эквивалент рефакторинга):

                "4ec063f5efded87f3e54760f46bfd6444c659e970c9039f32f00bf6dc6d9d"
                "4ec06778959537cf5c516021f1627238074a7dc54cc1b92894644c9cffb24"

                Ну что? Времени потребовалось в разы меньше?

                Все что универсально — проще для восприятия и понимания.

                Так вот if в данном случае совсем не Г, if задает легкочитаемую структуру кода, а Г — это ваш необдуманный комментарий.

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

                Конечно можно (и это лучше чем текст), но обсуждаем то, что в статье, а не все возможные альтернативы. И даже если будет возвращаться исключение это ведь по-хорошему ничего не меняет.
                Да и что вы будете делать, если в одном случае нужно исключение, а в другом просто выполнить какой-то алгоритм? Не надо пытаться натянуть ФП туда куда не нужно его натягивать.

                P.S.
                Последние два пункта даже не буду комментировать — там словесный понос.


                1. eabrega
                  03.06.2019 09:53
                  -2

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


                1. 0xd34df00d
                  03.06.2019 19:06

                  Все что универсально — проще для восприятия и понимания.

                  Чем меньше универсальности, тем проще понимать. Чем более ограниченным инструментом вы пользуетесь, тем меньше вариантов семантики кода.


                  Паттерны чуть более ограничены, чем ифы, поэтому то, что по смыслу является паттерн-матчингом, проще, лучше и понятнее делать на соответствующем синтаксисе.


            1. ApeCoder
              03.06.2019 09:58
              +2

              этот код легко читается

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


              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. Virviil
              03.06.2019 11:47
              +1

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


              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. Место — по всей видимости так-же не аргумент.



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


            1. Odrin
              03.06.2019 13:10
              +2

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


              1. math_coder
                03.06.2019 14:35
                +1

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


            1. 0xd34df00d
              03.06.2019 18:59

              Во-первых, pattern matching может проверяться тайпчекером на полноту, а последовательность ifов проверять на полноту сложнее.


              Во-вторых, пример из статьи скучноват. Давайте мы лучше возьмём, например, красно-чёрные деревья


              data Tree a
                = Empty
                | Node
                  { color :: Color
                  , left :: Tree a
                  , elt :: a
                  , right :: Tree a
                  }

              и функцию балансировки для случая вставки в левую подветвь:


              lbalance :: Color -> Tree a -> a -> Tree a -> Tree a
              lbalance B (Node R (Node R t1 e1 t2) e2 t3) e3 t4 = Node R (Node B t1 e1 t2) e2 (Node B t3 e3 t4)
              lbalance B (Node R t1 e1 (Node R t2 e2 t3)) e3 t4 = Node R (Node B t1 e1 t2) e2 (Node B t3 e3 t4)
              lbalance color left elt right = Node { .. }

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


              Ещё бы эффектнее это выглядело, если бы в х-ле был сахар для случая, когда правые части для разных паттернов выглядят синтаксически одинаково, но тут х-ль, увы, сливает некоторым ML'ам. Запилить, что ли...


    1. Virviil
      02.06.2019 09:06

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


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


      1. Nomad1
        02.06.2019 09:25

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


        1. Virviil
          02.06.2019 09:35

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


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


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


          1. Nomad1
            02.06.2019 09:43

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


        1. yarosroman
          02.06.2019 11:41

          Вы сравниваете изначально разные вещи, с различным предназначением. Для ЯП нормально взаимствовать друг у друга.


    1. 0x1000000
      02.06.2019 10:43
      +1

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


      1. 0x1000000
        02.06.2019 11:33

        Хотя есть один момент, что расстраивает:

        У дизайнеров было два выбора: либо молча сглатывать неохват паттерна делая default-init (иначе говоря, возвращать default(T) из свича у которого не замэтчился ни один паттерн) или же бросать исключение. Микрософт выбрали второе...
        Во многих ФП языках ветка по умолчанию обязательна, и это тот третий и самый, на мой взгляд, логичный вариант.


        1. 0xd34df00d
          03.06.2019 19:15

          Это в каких?


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


          1. 0x1000000
            04.06.2019 12:11

            Вообще согласен — такого действительно нет. Хаскель, кстати, тоже ошибку кидает Non-exhaustive patterns in case.

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

            Но это проблему можно (и на мой взгляд нужно) решить через предупреждение компилятора.


            1. 0xd34df00d
              04.06.2019 16:59

              Но по очевидным причинам в том же хаскеле у вас всегда будут либо false positives, либо false negatives.


              В Idris тоже, например, будут, для некоторого множества функций.


          1. math_coder
            04.06.2019 13:29

            там неплохой эвристический тоталити чекер

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


  1. AntonLozovskiy
    02.06.2019 09:59
    +3

    чем выше версия C# тем ужаснее становится синтаксис… хорошо хоть даже сами microsoft большинство этого ужаса не используют в примерах


    1. IvanNochnoy
      02.06.2019 11:11

      Это ответ автору статьи

      Все-таки с неймингом у МС получается ооочень плохо, прям фееричный булшит. Сначала nullable reference types — масло масляное, ибо референсные типы являются nullable по определению

      Серьёзно? Проблемы у Вас. По какому такому «определению»? Вот определение ссылочного типа в C#

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

      Где здесь упоминание о том, ссылочные типы обязаны быть nullable? Какая вообще связь между размещением объекта в куче и (не)возможностью объвлять этот тип nullable?


      1. Got_Oxidus
        02.06.2019 16:35
        +2

        Возможность размещать объекты в куче — означает nullable.
        Создаем ссылочную переменую и забываем проинициализировать. Что там будет храниться? Т.к. это ссылка, значит null, потому что дефолтное значение ссылки — null.
        Делаем вывод, что

        референсные типы являются nullable по определению


        1. khim
          02.06.2019 16:46
          +1

          Создаем ссылочную переменую и забываем проинициализировать.
          Хммм. Ok.

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

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


        1. Witch-hunter
          02.06.2019 17:12

          Создаем ссылочную переменую и забываем проинициализировать. Что там будет храниться?


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


          1. mayorovp
            02.06.2019 17:54
            +4

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


            1. Witch-hunter
              03.06.2019 00:08

              Нельзя, но если прочитать 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-значением?


  1. 0x1000000
    02.06.2019 11:00

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


    1. Witch-hunter
      02.06.2019 11:48
      +1

      Оно не совсем здравое

      1. В подавляющем большинстве случаев (> 90%) nullabe типы не нужны. Это означет, что все было бы утыкано воклицателными знаками, как будто программа кричит на программиста.

      2. Это привело бы к тому, что программисы просто ленились бы писать (!) и делали бы все типы nullable, что способствовало бы плохим практикам

      Текущее решение было тщательно продумано и обсуждено с общественностью.


      1. 0x1000000
        02.06.2019 12:53

        Я согласен, что вариант с "!" не идеален как и любое компромиссное решение, но им хотя бы можно было безболезненно начать пользоваться постепенно обновляя существующую кодовую базу.

        Сейчас же, многие просто не будут включать эту опцию в уже существующих проектах потому как понять, где нужно ставить “?”, а где не нужно, зачастую очень сложно.


        1. AgentFire
          02.06.2019 18:56

          где нужно ставить “?”, а где не нужно, зачастую очень сложно.

          как это так? объясните. имхо, это же очень просто


          1. 0x1000000
            02.06.2019 20:44
            +1

            Вот у вас есть существующий класс

            class MyClass
            {
               public string Id;
               public SomeType1 Property1;
               public SomeType2 Property2;
               ...
               public SomeTypeN PropertyN;
            }
            


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

            Какие поля являются «не обязательными» нужно выяснять разбирая код программы.

            Теперь я включаю эту новую опцию, и получаю кучу предупреждений при попытке собрать проект. Дальше есть 4 варианта:
            1) Забить на предупреждения (тогда зачем это все?)
            2) Пометить все поля как "?" (тогда зачем это все?)
            3) Отключить эту опцию (тогда зачем это все?)
            4) Потрать кучу времени на рефакторинг работающей и отлаженной программы (здорово конечно, но надолго ли вас хватит).


            1. AgentFire
              02.06.2019 23:18

              это вопрос про настройку большого старого проекта. да, здесь единственный 100% точный вариант — это просмотреть код, чтобы знать, что nullable, а что нет. (хотя по хорошему уже бы иметь какие-то маркеры, комментарии там, или атрибуты, чтобы декоративно было все видно).
              но если вы не хотите тратить время на подобную модернизацию большого и старого проекта, тогда зачем вы написали этот коммент?


              1. 0x1000000
                02.06.2019 23:50

                Эта ветка начиналась с напоминания о том, что были и другие варианты как реализовать nullable ref types.
                Идея помечтать обязательные поля "!" имеет обратную совместимость и не требует костылей в виде опций компилятора.


                1. AgentFire
                  02.06.2019 23:57

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


            1. Witch-hunter
              02.06.2019 23:48

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

              Вы же понимаете, что «Ошибку на миллиард долларов» так просто не исправишь? Процесс будет долгим и сложным. Если включить nullable контекст во всем проекте, то программиста завалит предупреждениями, это уже обсуждалось неоднократно. К счастью, контекст можно включать для отдельных участков кода с помощью директив, это позволит постепенно перевести проект на новые рельсы, путем постепенного увеличения кода в non-nullable контексте. Всё учтено могучим ураганом.


          1. ckaut
            02.06.2019 21:47
            -1

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


            1. AgentFire
              02.06.2019 23:16
              +1

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


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


              1. ckaut
                03.06.2019 12:47
                +2

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


      1. alex_zzzz
        02.06.2019 17:34
        +1

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


        1. 0x1000000
          02.06.2019 22:05
          +1

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

          int? = Nullable<int>

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

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

          string myVariable? = null

          a не

          string? myVariable = null


          1. ApeCoder
            02.06.2019 22:28
            +1

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


            1. 0x1000000
              02.06.2019 23:02
              +1

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


              1. mayorovp
                03.06.2019 00:50

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


                1. 0x1000000
                  03.06.2019 10:21
                  -1

                  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;}
                      }
                  


                  1. mayorovp
                    03.06.2019 10:31

                    Тут произошел конфликт названий, речь шла о другом виде типов. Статические типы (т.е. типы, известные компилятору) вовсе не обязаны совпадать с рантайм-типами.


                    Некоторые типы существуют только в исходном коде, а в рантайме полностью заменяются другими. Их-то ApeCoder и не вполне корректно обозвал "анонимными".


                    В языке C# уже были примеры таких типов — это dynamic, заменяемый на object; и кортежи, заменяемые на ValueTuple с потерей информации об именах свойств. Теперь появился ещё один класс таких типов, только и всего. Но типами они от этого быть не перестали.


                    1. 0x1000000
                      03.06.2019 11:23

                      Вот оригинальный текст:

                      Теперь самый важный вопрос: что же поменялось в IL? С точки зрения исполняемого кода — ничего! Но с точки зрения метаданных поменялось конечно: теперь в типе который использует nullable аннотации все типы которые являются nullable проаннотированы атрибутом [Nullable]. Сделано это по понятным причинам: чтобы потребители вашего кода могли использовать ваши аннотации.


                      Этот string существует и в исходном коде и в рантайме.
                      По сути запись:
                      public string? MyProperty { get; set; } = null;

                      это синтаксический сахар для:
                      [System.Runtime.CompilerServices.NullableAttribute]
                      public string MyProperty { get; set; } = null;


                      Кстати, по поводу:
                      … заменяемые на ValueTuple с потерей информации об именах свойств

                      имена свойств не теряются, они сохраняются в метаданных.


                      1. mayorovp
                        03.06.2019 11:26

                        Каким образом написанное вами опровергает тот факт, что string? — тип?


                        имена свойств не теряются, они сохраняются в метаданных

                        Они пропадают из информации о типе. Был тип (foo: int, bar: string), стал ValueTuple<int, string>


                        1. 0x1000000
                          03.06.2019 11:45

                          string? это тот же тип string, между ними нет разницы

                          но int? это действительно другой тип — Nullable.

                          Это как раз та путаница, о которой я говорил в предыдущих комментариях.


                          1. mayorovp
                            03.06.2019 11:49
                            -1

                            string? это тот же тип string, между ними нет разницы

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


                            1. 0x1000000
                              03.06.2019 12:11

                              второму нельзя присвоить null

                              Очень даже можно, просто компилятор выдаст предупреждение, что эта переменная помечена как «Not Null», а вы пытаетесь присвоить эй null.
                              Сам тип string символ "?" никак не меняет.

                              Более того, если я попытаюсь использовать сборки скомпилированные в C#8 из C#7, то все поля помеченные как «string?» будут видны как обычные «string» и накаких предупреждений (или ошибок компиляции) не будет, если я попытаюсь присвоить им null.


                              1. mayorovp
                                03.06.2019 12:27

                                А всегда ли это будет предупреждением, или это просто текущее состояние компилятора?


                                Более того, если я попытаюсь использовать сборки скомпилированные в C#8 из C#7

                                Естественно, что в разных языках разные системы типов.


                              1. ApeCoder
                                03.06.2019 14:01

                                Сам тип string символ "?" никак не меняет.

                                "Тип данных (тип) — множество значений и операций на этих значениях."


                                Так как набор операций изменяется, то это уже другой тип.


                                эта штука не меняет тип String, но она обозначает тип "nullable string" который может быть проассоциирован с переменной. Так же как и int[10] не меняет тип int, а делает новый тип по int и количеству элементов.


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


                                1. 0x1000000
                                  03.06.2019 14:21

                                  Если бы «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?)


                                  1. mayorovp
                                    03.06.2019 14:26

                                    Вы так пишете, как будто смарт-касты в других языках ещё не изобрели...


                                  1. Witch-hunter
                                    03.06.2019 14:36

                                    Если бы «string?» был другим типом, то требовалось бы приведение типов

                                    Ну, не факт, это зависит от языка, скажем, в F# нельзя неявно привести Int32 к Double, а в С# можно.

                                    А что рефлекшн говорит по поводу «string?»?


                                    1. DistortNeo
                                      03.06.2019 14:43
                                      +1

                                      А что рефлекшн говорит по поводу «string?»?

                                      Что это обычный string. А атрибут относится не к типу, а к переменной.


                                    1. 0x1000000
                                      03.06.2019 14:44

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

                                      Я такое приведение могу сделать и для других типов определяя оператор «implicit».

                                      А что рефлекшн говорит по поводу «string?»?

                                      Ответ в статье:
                                      Теперь самый важный вопрос: что же поменялось в IL? С точки зрения исполняемого кода — ничего!


                                      1. mayorovp
                                        03.06.2019 20:11

                                        Статические типы (т.е. типы, известные компилятору) вовсе не обязаны совпадать с рантайм-типами.


          1. alex_zzzz
            03.06.2019 02:26

            Есть готовый синтаксис:


            int i1 = 10;
            int? i2 = null;

            ну и нечего изобретать велосипед:


            string s1 = "10";
            string? s2 = null;

            Кто учит язык и пока не знает, как это реализовано, тому и так нормально: суть похожая и внешне выглядит одинаково.


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


  1. anaym
    02.06.2019 11:48
    +2

    Поэтому он теоретически может быть отрицательным, хотя конечно в C# запись x[-1] лишена всякого смысла.

    Не лишена.
    Во-первых — можно перегрузить индексатор в вашем классе и корректно обрабатывать в нем отрицательные индексы.
    Во-вторых, в C# массивы могут индексироваться не только с 0:
    var x = (int[,])Array.CreateInstance(typeof(int), new[] { 42, 42 }, new[] { -1, 2 });
    x[-1,3] = 1234;
    


    1. AgentFire
      02.06.2019 19:01
      -2

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


  1. blaka
    02.06.2019 11:48

    А будет ли в c# аналог lateinit того же Котлина?


    1. mezastel Автор
      02.06.2019 11:56

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


      1. blaka
        02.06.2019 12:10

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

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


        1. mayorovp
          02.06.2019 17:56

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


          1. blaka
            02.06.2019 18:15

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


            1. ApeCoder
              02.06.2019 20:01

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


              1. blaka
                02.06.2019 21:26

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

                Инициализация в конструкторе это костыль
                1. Я уже говорил, что эта инициализация просто нагружает GC лишний раз. Можно сказать что это короткоживущие объекты, но есть пункт 2
                2. Может быть совсем не просто инициализировать свойство, которое суть сложный объект, да еще с приватным/интернал конструктором (сериализатору это без разницы, а вот вы будете в тупике)
                3. И даже если можно, то в случае проблем с десериализацией вы можете получить неконсистентное состояние (новые данные + бесполезное дефолтные)

                А теперь про гарантии. Вдруг десериализатор не заполнит некоторые свойства. Сравните:
                а) В случае lateinit я просто сделаю проверку на null всех свойств 1 раз (и все) после десериализации. Я выполню гарантии в рантайме и у меня есть способ сказать компилятору «я сам».
                б) в случае «дефолтные в конструкторе» я не смогу отличить что новое, а что старое. Мне придется делать полный валидатор, что уже перебор. Также он может быть невозможен, если значение свойства не отличить — и тогда придется все делать nullable. А это приведет к тому, что в 1000 мест мне придется проставлять!

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


                1. ApeCoder
                  02.06.2019 22:33

                  Возможно, он просто не успел. В Котлине это сразу было или потом добавили?


                  Логически правильно тогда либо не позволять создавать объекты такого типа никак кроме как десериализацией (и допустим вручную, но с заполнением всех полей). Либо десериализация возращает тип, который знает, что все поля заполнены. Хотя наверное, так все вообще сложно будет.


                  1. blaka
                    02.06.2019 23:41

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

                    Пример реальной попытки: сериализатор требует пустого конструктора и заполняет только те свойства, что может заполнить (причем через internal сеттеры). Если не заполнены, то это нештатная ситуация и простой валидатор проверяет на null зная, что ни одно поле не может быть null. Сложный валидатор невозможен — ему тут не место. И все работает, все хорошо, только фатальный недостаток — не использует новых фич

                    Пробую #nullable. Инициализация дефолтными значениями не прошла. Сразу поломался валидатор на незаполненность, а больше ничего и не проверить. Попытка обвесить модельки #nullable disable тоже ничего не дало. В итоге сделал свойства моделек nullable и начал массово (реально в сотнях мест) проставлять! но на полпути задался вопросом «а зачем?»

                    Код остался прежним, но я получил требование в клинтском коде писать! (и необходимость помнить, что! обязателен, ведь non-null гарантирован логикой, nullable из-за компилятора), а вот выгоду я увидеть не смог

                    и придумать как переделать, да чтобы не городить жуткие костыли из-за слабости поддержки nullable в языке, я тоже не смог, поэтому вернул все обратно.


                    1. Bronx
                      03.06.2019 00:16

                      По-идее, клиентский код (в смысле business logic) не должен видеть все эти DTO, т.е. по хорошему у вас в доменной модели должен быть тип (скажем, class Person{}), который обеспечивает все гарантии nullability и прочего, и который конструируется из DTO (скажем, class PersonDTO{}) имеющий пустой конструктор, nullable поля и ноль гарантий, т.е. простой мешок пропертей. Валидация входных десериализованных данных происходит один раз в конструкторе Person(PersonDTO dto), а дальше гарантии распространяются через этот Person.


                      1. blaka
                        03.06.2019 04:13

                        Все зависит от точки зрения. Ваша цепочка выглядит примерно так:

                        --> json --> десериализация в PersonDTO --> Person(PersonDTO dto) -->

                        «десериализация в PersonDTO» может быть библиотекой-клиентом к некому апи, которую клиентский код может использовать. Она, можно сказать, транспортный слой, который при этом ничего не знает про business logic. И нормально, что этот слой валидирует полное получение данных, хоть и не может валидировать их суть. Я получаю ошибку сразу «fail fast», и к тому же на уровне, где она произошла.

                        Если я получаю из api погоду в виде иммутабельного объекта Weather c 3мя свойствами «город, температура, влажность», то рассматривать Weather как dto и создать WeatherModel, или просто использовать как есть? — каждый решает сам, но в случае «использовать как есть» у нас все равно есть гарантия транспортного слоя, что «город не null» и «температура настоящая, а не дефолтная»

                        И если переделка под nullable привносит только необходимость прописывать восклицательные знаки (или же отказ от проверок транспортного слоя), то я лучше откажусь от nullable


                        1. Bronx
                          03.06.2019 08:38

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

                          Ну вот я собственно и сомневаюсь в том что транспортный слой должен давать какие-то гарантии модели, помимо типа и формата данных (число, строка, дата, объект, массив и т.п.). Это как бы не его роль. Его роль — тупо сконвертировать данные из одного представления в другое "as is" и дальше дать решать бизнес-слою, полные это данные или нет. Может оказаться, что ему и не нужна абсолютная полнота, что он терпит частичные данные, и если у Weather не будет температуры, то и фиг с ней, он её не использует.


                          Тем более что частичные DTO — это распространённый метод обновления лишь некоторых выбранных значений вместо посылки полной модели..


                          1. blaka
                            03.06.2019 12:46

                            Ну это мы в сторону ушли (моя вина). Не должен транспортный давать гарантии модели. Он должен давать гарантии получения данных «не потерялось и дошло как надо». Но мы ушли в обсуждение архитектуры, а ведь проблема куда проще.

                            Речь идет о сериализаторе. К нему есть требование дать гарантию, что он заполнит все поля в классе, тип которого ему передадут для материализации (и они не будут дефолтными). Это у него постусловия такие. И сериализатор достаточно глуп, чтобы отследить это прямо в процессе разбора данных. Приходится проверять постусловием
                            Эти постусловия четко отражены в спеке и следуя «концепции баррикады» можно не писать дополнительные проверки на это в клиентском коде и не получить NRE.

                            и с #nullable в их текущей реализации выбор очевиден
                            1 инициализировать дефолтные и поломать постусловия, от чего пострадает клиентский код (breaking change) — и все это с нулевой выгодой
                            2 заставить в клиентском коде проставить везде "!" и заставлять в новом коде это писать — и снова выгоды не вижу
                            3 оставить все как есть.


                            1. Bronx
                              03.06.2019 20:53

                              Сериализатор говорит: у меня Weather.City всегда не null. И тут приходит HTTP PATCH с частично заполненным JSON, в котором нет поля city (потому что на клиенте оно не менялось). Какова реакция сериализатора? Выкинуть исключение, потому что, по мнению сериализатора, без city этих данных недостаточно для работы? А с чего он взялся решать такие вопросы? А бизнес-слой считает, что ему данных хватает, но между клиентом и ним сидит чересчур умный сериализатор и не даёт данным дойти.


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

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


                              1. gdsmiler
                                03.06.2019 21:30
                                +1

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


                                1. Bronx
                                  03.06.2019 22:04

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


                              1. blaka
                                03.06.2019 21:47
                                +1

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

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

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

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


                                1. Bronx
                                  03.06.2019 22:22

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


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

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


                                  1. 0xd34df00d
                                    04.06.2019 01:30

                                    Я, наверное, упустил контекст, но зачем вся эта ерунда, если прямо сериализатору можно сказать об ограничениях на значения через типы? Если там поля может не быть, то, значит, тип поля — Maybe a/std::optional<T>/etc.


                                    1. Bronx
                                      04.06.2019 04:51

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

                                      Такая вот попоболь.


                                      1. DistortNeo
                                        04.06.2019 09:49

                                        Не очень понял, за что минус. Конечно, вы можете написать свой Optional и свой сериализатор (как я сделал для своих задач), но это уже велосипедостроительство. Будет сильно лучше, если эти типы будут присутствовать в стандартной библиотеке в пространстве имён System.


                                        1. 0xd34df00d
                                          04.06.2019 17:00

                                          Я вообще не могу рассуждать о C# и .NET, но для своего ORM'а на плюсах я сделал обёртки типа PKey, Unique, NotNull и подобные, которые движку сообщают всю нужную семантику.


                                          Но за что минус, я тоже не понимаю.


                                        1. ApeCoder
                                          04.06.2019 22:20

                                          Чем именно вот это https://github.com/microsoft/referencesource/blob/master/mscorlib/system/nullable.cs отличается от option?


                                          1. DistortNeo
                                            04.06.2019 22:30

                                            Тем, что where T : struct.


                                            1. ApeCoder
                                              05.06.2019 07:15

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


  1. ckaut
    02.06.2019 12:18
    +1

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


  1. blaka
    02.06.2019 12:30
    +1

    Nullable Reference Types — то, что должно быть изначально. В котлине он изначально и там с этим очень неплохо. А вот в c#… прямо сейчас я чиню очередной NRE, причем в библиотеке, а ведь его могло бы и не быть, будь от этого защита.

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

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


  1. lorc
    02.06.2019 13:14

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


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


    1. mezastel Автор
      02.06.2019 16:38

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


  1. KvanTTT
    02.06.2019 16:02
    +3

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


    1. vortupin
      02.06.2019 18:13
      -3

      Это вполне нормальное явление на Хабре: люди, не имеющие возможности приобрести даже самый дешевый автомобиль от «Tesla», долго и со смаком рассуждают о преимуществах Model X, или«учат» Илона Маска, как нужно вести бизнес, или делать ракеты. А уж сколько появляется «экспертов» в комментариях любой «болтологической» статьи (будь-то про job interview, жизни за рубежом, seniors vs juniors etc.)…


    1. mezastel Автор
      02.06.2019 22:15
      +4

      Ну, люди пишут в соответствии с тем фактом, что им с этими фичами работать.


      1. vortupin
        03.06.2019 04:46
        -1

        Ой, я вас умоляю, «работать»… Если человек реально заинтересован в какой-то новой фиче, уверен в своей правоте, и может эту правоту доказать не «минусованием кармы» оппонента (на гитхабе анонимные пакостники не в почете), а аргументированным feature request-ом, а, еще лучше, pull request-ом, то он не будет «чесать» свое ЧСВ тут, в комментариях — что, по сути, занятие бессмысленное и полностью бесполезное. Но я глубоко сомневаюсь, что кто-то из здешних комментаторов выдал хоть один PR в "репу".


  1. Xandrmoro
    02.06.2019 16:20
    +1

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

    А вот, например, паттерматчингом я никогда не пользовался (разве что is null вместо == null) и вообще не вижу зачем он, но мб зачем-то и нужен.


    1. mezastel Автор
      02.06.2019 16:39
      +1

      Поведенческие миксины уже существуют давно как методы-расширения-на-интерфейсах.


  1. alex_zzzz
    02.06.2019 16:30
    +1

    хотя конечно в C# запись x[-1] лишена всякого смысла

    Если речь только только про массивы, то да. Если про C# вообще, то нет. Смысл зависит от того, что есть x. Самописный индексатор можно делать всё что хочет. Даже без самописных индексаторов я собственными руками писал x[-1], где x был указателем.


  1. alex_zzzz
    02.06.2019 16:35

    Встроенные типы, такие как массивы или строки, конечно же поддерживают индексер (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


    1. mezastel Автор
      02.06.2019 16:40

      Смею предположить, что вы проверяете на VS2019 RTM. А надо на Preview.


      1. alex_zzzz
        02.06.2019 16:48

        Microsoft Visual Studio Community 2019 Preview
        Version 16.2.0 Preview 1.0

        NET Core 3.0.100 Preview 5


        1. HavenDV
          03.06.2019 10:52
          -1

          Есть статья на английском хабре, там сказано:

          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


    1. mentin
      03.06.2019 08:33
      +2

      Это, кстати, соответствует майкрософтовской документации (см мой коммент выше), что ^0 это индекс за-концом массива (а не последний как в статье), ^1 это последний элемент, а ^2 предпоследний.


    1. HavenDV
      03.06.2019 18:39

      Эти результаты правильны согласно официальной документации:

      Для любого числа n индекс ^n совпадает с sequence.Length — n


      Возможно, в предыдущих Preview было иначе.


  1. alex_zzzz
    02.06.2019 16:43

    Представьте массив 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


    1. mezastel Автор
      02.06.2019 17:52

      Тут я действительно ошибся. Конечно ..2 даст тот же результат, должно было быть ..^0.


      1. mentin
        03.06.2019 08:30

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


  1. danfe
    02.06.2019 17:44
    +1

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


  1. Ascar
    02.06.2019 17:50
    -2

    Норму по нововведениям выполнили. Мне кажется скоро весело придут к обертке над ссылочными типами которая не может быть null. CanNotBeNullable<>


  1. mjr27
    02.06.2019 21:44
    +1

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


    Мнение:


    • nullable — сделано совсем через задний проход. Если по дефолту сделать nullable, в происходящем появится хоть какой-то смысл. Имхо сильно на любителя.
    • Index & Range — звучит безумно, но пальцы привыкают быстро.
    • Default interface members — минимум 90% разработчиков это не понадобится никогда. И слава богу.
    • Pattern matching — ура. просто ура.


    1. blaka
      02.06.2019 22:00

      Если по дефолту сделать nullable
      Ну так оно и так во всем существующем коде по дефолту nullable. И все эти годы так было. И именно это «по дефолту null и инициализации не было и никто не подсказал про забытую инцицализацию» и является основной причиной ошибок NRE.

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

      А так лучше бы они records сделали — крайне полезная вещь.


      1. mjr27
        03.06.2019 09:42

        Лучше бы они records сделали
        Это да. Я очень расстроился, когда понял, что все слухи о них оказались слухами.

        А по поводу nullable моя претензия заключается в том, что данные, приходящие из legacy (#nullable disable) кода, почему-то считаются non-nulable, хотя обычно это не так. И за счет этого мечта о "прощай, внезапный NRE" остается мечтой.


    1. alex_zzzz
      02.06.2019 23:06

      У меня реакция такая:


      • 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 ? Кому-то точно это принципиально.


  1. IL_Agent
    03.06.2019 00:42

    Как же жаль, что Котлин так и не запилили под дотнет (


    1. alex_zzzz
      03.06.2019 01:07

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


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


      1. ApeCoder
        03.06.2019 10:04

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


    1. mezastel Автор
      05.06.2019 01:16

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


  1. ad1Dima
    03.06.2019 05:51

    Про индексы, и прочее. Там все немного поменялось же:

    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.


  1. Vanadium
    03.06.2019 10:54
    -1

    Классная статья, очень интересно читать, хотя я даже не пишу на сишарпе


  1. RomanPokrovskij
    03.06.2019 16:11

    Знатоки, напишите статью, и поделитесь пожалуйста примерами до чего дошел Pattern Matching в F# или других языках развивающих эту концепцию? Хотелось бы понять «о чем мечтает команда разработчиков C#» и к чему стремится прогресс. И конкретно, а где нибудь уже дошли до разбора выражений языка средствами языка?

    Отдал был часть кармы если бы Хабр позволял «краудфайндить» на статьи.


    1. ApeCoder
      03.06.2019 16:20

      1. RomanPokrovskij
        03.06.2019 23:58

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

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


        1. math_coder
          04.06.2019 03:33

          При чём здесь рефлекшен?


          1. RomanPokrovskij
            05.06.2019 00:12

            Еще предстоит разобраться что было с головой, что так повело рассуждение. Ошибка. Спасибо что обратили внимание. Сейчас бы написал «немного полезного синтаксиса вокруг условных присваиваний».


    1. 0xd34df00d
      04.06.2019 17:06

      Знатоки, напишите статью, и поделитесь пожалуйста примерами до чего дошел Pattern Matching в F# или других языках развивающих эту концепцию?

      Хаскель — pattern guards (очень удобно, кстати, регулярно пользуюсь), view patterns (тоже очень удобно, но реже), pattern synonyms (удобно при написании eDSL'ей).


      Idris — dependent pattern matching, или та же идея чуть более формально.


      И конкретно, а где нибудь уже дошли до разбора выражений языка средствами языка?

      Template Haskell?


      Elaborator reflection (но это я пока ещё не осилил)?


    1. mezastel Автор
      05.06.2019 01:18

      У меня есть один продукт, написаный на F# с использованием активных рекурсивных паттернов. Посли полгода я не могу читать свой собственный код. И этим все сказано. Фичи действительно очень мощные, но им грош цена если они порождают write-only code.


      1. IvanNochnoy
        05.06.2019 14:34

        И этим все сказано

        Что именно сказано? Что Вы пишете такой код, что не можете его прочитать?