Pull to refresh

Comments 145

Safe Navigation Operator по-моему самое ожидаемое нововведение.
Но и остальные тоже будут крайне полезны для уменьшения количества строк кода.
Воистину нуллчеки долгожданны. Остальное полезно, но слово «крайне» здесь явно лишнее )
Оставлю для справки наиболее элегантный заменитель Safe Navigation Operator в текущей версии C#

Использование:
var name = user.NoNull(x => x.Company)
  .NoNull(x => x.ParentCompany)
  .NoNull(x => x.Name, "N/A");

var total = user.NoNull(x => x.Company)
  .DefaultForNull(x => x.Users)
  .SelectMany(x => x.Email)
  .Where(x => x.EndsWith("@gmail.com"))
  .Count();


Реализация:
    public static TResult NoNull<TObject, TResult>(this TObject obj, Func<TObject, TResult> accessor, TResult defaultingTo = default(TResult)) {
        if (ReferenceEquals(obj, null))
            return defaultingTo;
        return accessor.Invoke(obj);
    }

    public static IEnumerable<TResult> DefaultForNull<TObject, TResult>(this TObject obj, Func<TObject, IEnumerable<TResult>> accessor, IEnumerable<TResult> defaultingTo = null) {
        if (ReferenceEquals(obj, null))
            return defaultingTo ?? Enumerable.Empty<TResult>();
        return accessor.Invoke(obj);
    }

«NoNull» звучит очень странно.
Странно, что есть в планах такие сомнительные вещи как Methods Expressions и Primary Constructor, но при этом нету теплых ламповых константных методов. Еще хотелось бы generic ограничения на бинарные и унарные операторы (+-*/!~), векторизацию в ngen бы еще. Последние два решили бы много проблем с производительностью, а константные методы позволят сильно упростить понимание некоторых участков кода.
ngen — вне компетенции команды языка C#. Все представленные здесь изменения — они в компиляторе, а не в среде.
Подозреваю, что плюсовый const заклеймили как избыточную сложность, так же как и джавовый throws (с единственным отличием, что const хотя бы работает).

С дженериками и операторами непросто… Вон, в плюсах «концепты» понапридумывали, а потом выпилили, потому что оказалось сыро. Нельзя же просто «operator==» рядом с «new()» в ограничениях писать — бред получится.
Можно сделать специальный интерфейс для обозначения «у класса реализованы арифметические операторы» или более конкретно для каждого отдельно или более узких групп. Т.к. операторы статические, то такой интерфейс скорее будет пустым и должен иметь поддержку со стороны среды или компилятора. Вообще говоря, подобной магии хватает в дотнете — не думаю, что такая реализация казалась бы слишком костыльной.

Через десять лет - появилась обобщённая математика в c#.

И почему векторизацию в ngen, а не в CLR?
А константные методы позволили бы например оптимизировать чистые функции?
Если добавить в JIT'тер — будет долго.
Можно использовать атрибут Pure из Code Contracts для обозначения константных методов — какая-никакая, да альтернатива.
Хм, как мне кажется, синтаксис
public int X { get; } = x; 
совершенно не нужен в свете синтаксиса
public int X => x;

Или он еще и x объявляет? Но тогда я вообще не вижу отличий от
public int X { get; private set;}
x — это параметер Primary Constructor-а:
public class Point(int x, int y)
{
    public int X { get { return x; } }
    public int Y { get { return y; } }
    public int Dist { get; } = Math.Sqrt(x * x + y * y);
}
Подправил пример, чтобы сделать его более очевидным.
Я бы добавил синтаксис инлайн инициализации свойства. Например:
public int X { get; private set;} = 100;

должен раскрываться в:
private int _x = 100;
public int X { get{ return _x; }; set { _x = value; } }

а такое:
public int X { get; } = 100;

в следующее:
private readonly int _x = 100;
public int X { get{ return _x; };}
J>зачем вводить синтаксис public int X { get; } = x если и так можно будет написать public int X => x;?

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


rsdn.ru/forum/dotnet/5401930.1
Правда, непонятно зачем это нужно, учитывая, что IEnumerable можно передавать в методы и без ключевого слова params.

Ну как же, можно будет написать
void Test(params IEnumerable<int> ints)
и вызывать его как
Test(1,2,3)
так и
Test(any_collection_instance)
В то время как сейчас второй вызов придется делать
Test(any_collection_instance.ToArray())
Спасибо. Я почему-то пытался увязать это с другими вещами. В случае с методами с переменным числом параметров мне кажется странным, что нельзя делать так:

public void Test(params int[] arr)
{

}
...
int[] arr1 = new int{1,2,3};
int[] arr2 = new int{3,4,5};
Test(arr1,arr2);//ошибка

Есть что-то в вашем примере.
Спасибо LINQ, он хоть как-то позволяет сделать то о чем вы говорите:
Test(arr1.Concat(arr2));
Но, я думаю, вы вкурсе об этом=)
Очень давно жду, когда появится возможность писать что-то вроде
var c = match(s) { case 1 => "One" case _ => "Two" }
как в Scala и других языках.
Очень жалко, что не добавляют
Pattern matching многие давно ждут, однако он есть в F#, а сборки написанные на нем можно интегрировать с любыми другими.
Если нужен суровый паттерн-матчинг — выносится в модуль на F# и подключается.
PM — это не что-то без чего нельзя писать код. Это просто стиль к которому привыкаешь. Он лаконичен и довольно элегантен.
Разбивка логики на C# класс и F# модуль только ради этого — попахивает бредом. Да и когда в команде не все знают F# делать так — идиотизм.
Если я не ошибаюсь, то F# Runtime придется тащить за собой. Да и не охото менять язык из-за одного match, который проще съэмулировать в C#.
Извините меня, но что вас заставило в «съэмулировать» вставить твёрдый знак?
твёрдость «с», ясное дело :)
О, отличные нововведения. Primary constructor, method expressions, safe navigation operator — класснейшие штуки.
Еще static type using statements добавили (в статье не упомнянут, есть по ссылкам): using System.Math;

сорри, не заметил
Есть же, перед Property Expression :)
Первичный конструктор — я так понимаю, поля будут такого же названия как и аргументы? а что если я привык к нижнему подчеркиванию — писать аргументы конструктора с ним? :-( А так изменения классные!
Поля автоматически не создаются, как я понимаю, можно будет сделать как-то так:
class TestClass(int x, int y)
{
int _x = x;
int _y = y;
}

хотя судя по всему можно будет позволить и создать поля компилятору
Логика видимо такая. Если переданный в конструктор параметр не используется внутри конструктора, неявно автоматически объявляется поле с тем же именем и ему присваивается значение параметра.
Кстати, это поле объявляется как readonly, или его можно затем модифицировать? Логичнее readonly, как мне кажется.
Все же самая бомба с точки зрения сокращения количества кода — это "?."
На всякий случай удалил лишнее предложение с раздела об автосвойствах, чтобы не подумали, что я считаю это самым ожидаемым нововведением. Просто очень часто встречал (в том числе и на англоязычных сайтах), что многим не хватает такой возможности.
Я встречал «return value as object;», это пока никому не удавалось превзойти :) Перестраховщики, не знающие язык, будут всегда.
А вы уверены, что тут вообще потребуется "?? null"? Было бы логично реализовать A?.B() как (A==null? null: A.B()), без обязательного требования ?? (в случаях, когда результат A.B() — reference type). И код выглядел бы так:
string name = points?.FirstOrDefault()?.Name;


Но интересно, будет ли работать
string SX=points?.First()?.X?.toString() ?? "No points";


А ещё —
x=points?.[0]?.X ?? -1;

string SX=points?.First()?.X?.toString() ?? "No points";


Такая конструкция работать не будет скорее всего, так как First бросит исключение, если коллекция пустая, а не вернет null.
Вот именно поэтому надо использовать FirstOrDefault, будет счастье.
Согласен. Но будет ли? Какой вообще тип у выражения «point.?X»? Неужели «int?»?
Поскольку "?." — это просто сахар для более многословной конструкции, то мне кажется, что если «point.X» — int, то и «point?.X» обязан быть int. На случай point == null справа от выражения должен стоять оператор "??". Тогда можно будет вернуть, например, 0.

Если же мы хотим nullable тип, то на это придётся указать явно:

int? x = point?.X ?? null;
либо
var x = (int?)(point?.X) ?? null;

По крайней мере лично я ожидал бы именно такого поведения.
var x = point?.X; — это синтаксический сахар вместо
var x = point == null ? (int?)null : point.X;

тип x будет int? без всяких ?? справа от выражения.
Конструкцию ?. — вводят для того, чтобы получать null вместо NullReferenceException.
Если тип x будет int, как вы предположили, то весь смысл конструкции потеряется — потому что вести себя она будет точно так же, как point.X.
Я почему-то исходил из предположения, что в С# "??" будет обязательной частью цепочки с "?." :)
Видимо, смутил пример использования.

Признаю, вы правы.

Таким образом,
«point?.X» — это int?, а
«point?.X ?? 3» — это int.
Да, логично.
Я почему-то исходил из предположения, что в С# "??" будет обязательной частью цепочки с "?." :)

А пока ниоткуда не следует, что это не так :) За исключением, разве что, отсылки к Groovy
Да. И да, будет. Нормальное такое выведение типов, очень в функциональном духе.

(и да, тип у выражения point?.X ?? 3, если X — int, должен быть int)
А на мой взгляд использование статических классов в директиве using ненужная штука. Получается какой-то уход от объектно-ориентированного программирования к функциональному.

Объявление out параметров в вызываемом методе вообще как-то не читабельно будет выглядеть:

int.TryParse("123", out int x);
Console.WriteLine(x);

А вдруг метод будет иметь 5-6-7 параметров? Тогда пол часа будешь искать где же объявлена переменная (если не тыкать F12)

Из всех ожидаемых реально клёвым было бы использовать оператор безопасной навигации
Получается какой-то уход от объектно-ориентированного программирования к функциональному.

А extension-методы вас не смущали? По-моему, вполне логичное дополнение к extension.

А вдруг метод будет иметь 5-6-7 параметров? Тогда пол часа будешь искать где же объявлена переменная (если не тыкать F12)

Опять же, с extension-методами еще запутаннее. Выглядит как метод типа объекта, а на самом деле может быть объявлен вообще где угодно. И самая большая беда — если нет reference на сборку, где объявлен этот метод, то вообще невозможно по коду понять, где этот метод объявлен.
В случае using достаточно посмотреть в начало файла.

К тому же, в C++ унаследованы глобальные методы из C, и ничего, вроде никто не называет это функциональщиной…
А extension-методы вас не смущали?
Да… Было дело… Как-то в чужом коде долго пытался на MSDN найти описание одного extension-метода к одному стандартному фрэймворковскому классу)))) Но ведь речь не об этом. Речь о том, что на NDC предложили, опять же на мой взгляд, язык еще чуточку запутать. Лично я, в отличие от вас, не улавливаю логику связи юзингов и икстеншинов.
А вот если несколько функциональных класса реализуют метод с одинаковым именем, то потенциально создаются неприятности с копипастом кода)

К тому же, в C++ унаследованы глобальные методы из C, и ничего, вроде никто не называет это функциональщиной…
Ну давайте еще вернем их плюсов директиву препроцессора #include))))
А вот если несколько функциональных класса реализуют метод с одинаковым именем, то потенциально создаются неприятности с копипастом кода)

Вообще говоря, неймспейсы — это тоже синтаксический сахар сишарпа, в CLR их нету — System.String это полное имя класса и ничего, живем. С одинаковыми именами внутри неймспейса и т.п. Я не сильно рад юзингу статических классов, но и страшного в нем нечего тоже, думаю, нету. Для разрешения конфликта имен наверняка так же можно будет использовать алиасы.
Кстати, многие об этом не знают, но в Java еще с 5 версии есть аналогичная возможность не только для классов, но и для отдельных методов и целых package, например так:
import static com.foo.myMethod;

И далее использовать также, как статический метод собственного класса:
myMethod();

Подробнее:
docs.oracle.com/javase/1.5.0/docs/guide/language/static-import.html
>>А вдруг метод будет иметь 5-6-7 параметров?

Более двух out параметров уже запашок от метода имхо.
объектно-ориентированность и функциональщина — не альтернативы друг другу, а ортогональные понятия. так что «ухода» от одного к другому быть не может
Это больше для ДСЛ всяких. В джаве вон ассерты в юниттест фреймворках так обычно импортируют.
Мне как любителю дженериков больше всего вывод типа для конструктора generic класса понравилось, тоже думал об этом раньше. Жалко, что не добавляют ограничение для дженериков на параметризованные конструкторы — как было бы здорово where T: new(string) или where T1: new(T2, T3)
А вот этого реально не хватает
А еще все никак не добавят ограничение дженериков на enum и delegate, плюс protected AND internal вдобавок к текущему protected OR internal, хотя все три фичи есть в CLR.
Вот спорят часто, что C++ мол-де сильно осложнился в последнее время. Но, если посмотреть, то любой язык, что C++, что C#, пойдёт по пути гибридизации и усложнения, это неизбежно, как и потребность разработчиков в понимании более сложных концепций и большего их числа.
После linq и await у них видимо фантазия закончилась, один сахар синтаксический.
Да, ничего революционного. Меня это удивляет, потому что революций для совершения — дофига и больше, и у разрабов нехилый список идей за все эти годы накопился. Из первого, что в голову приходит:

1. Extension everything (см. F#)
2. Macros (см. Nemerle)
3. Mixins, traits, aspects

Всё это поменяет написание кода, а не просто сэкономит пару строчек.
На UserVoice идеи присутствуют? Было бы неплохо, если бы кто-то указал, где можно подать голоса за это.
Юзервойс по C# — для мелких безделушек. В новой версии шарпа основная революция — компилятор, поэтому и решили обратить внимание на мелочи, чтобы порадовать простых смертных хоть чем-то, чтобы не было воплей: «А где новые фичи-то?!».

Революции будут, когда разрабы решат, что для них настало время. Список у них безо всяких юзервойсов есть, давно. Новый компилятор будет, так что ждём подвигов в 6.5 и 7.0.

Вообще, если поискать, то можно найти на юзервойсе extension properties и ещё что-то по мелочам, но я бы не тратил на это время.
А вывод типов для дженериков будет фишкой компилятора или фишкой CLR?
Вывод типов для дженерик методов сейчас реализован на стороне C# — для конструкторов наверно так же сделают, чтобы зря CIL/CLR не усложнять.
А как вывод типов вообще может быть фишкой CLR, а не компилятора?
Вероятно имелся в виду не только CLR, а еще и CIL вместе с ним. При компиляции CIL проверяются ограничения на дженерики (в спеке есть подробное описание). Так что, по идее, можно спустить вывод типов на уровень CIL.
Задумался над вашим ответом, думаю, я понял идею. Но заметьте — вы сказали «при компиляции CIL», а значит, даже если бы CIL тоже имел синтаксический сахар в виде возможности опустить тип, который и так известен, это всё равно компилятор — тот, кто занимается выводом типов ;)
Так стоп :) Я имел в виду JIT-компиляцию сгенерированного CIL кода (т.е. компиляцию при запуске приложения), уже после того, как C#-компилятор создаст сборку. На том уровне все по минимуму — метаданные, стэковые команды + валидация. Если туда сахар добавлять, то у вас все .NET приложения будут запускаться/работать медленнее.
Ну, вообще, я всегда считал что при выводе типов компилятор добавит в CIL явное указание дженерик аргументов, т.е. запись «s(s2 any)» превратится в конструкцию, эквивалентную «s(s2 any)» на C#.
UPD: И, да, это описка, я имел ввиду CIL а не CLR.
А как вывод типов вообще может быть фишкой CLR, а не компилятора?
Подробнее в этой статье можно прочитать, как в Java чисто компиляторная фишка, а в C# — рантаймовая.
За статью спасибо, но там о другом. Вывод типов — это когда я вместо
Tuple˂int, int˃ x = Tuple.Create˂int, int˃(1, 2);
пишу:
var x = Tuple.Create(1, 2);
и компилятор понимает, что 1, 2 — это int, значит я вызываю Create˂int, int˃, значит x — это Tuple˂int, int˃.
Да там много чего интересного можно было бы сделать. Например
        private delegate void Del1();

        private delegate void Del2();
        static void Main()
        {
            Del1 del1 = () => Console.WriteLine("Foo");
            Del2 del2 = (Del1) del1;
        }


то есть считать делегаты с одной сигнатурой — алиасами одного и того же делегата. Но нет, это ведь так сложно…

Я уже молчу про IList. Как один умный человек, насколько лучше было бы изначально сделать такие интерфейсы:

  • Just Enumeration IEnumerable(T)
  • Readonly but no indexer (.Count, .Contains,...)
  • Resizable but no indexer, i.e. set like (Add, Remove,...) current ICollection(T)
  • Readonly with indexer (indexer, indexof,...)
  • Constant size with indexer (indexer with a setter)
  • Variable size with indexer (Insert,...) current IList(T)


а не наследовать массивы от IList(T), а потом бросать исключения при вызове половины методов…
> то есть считать делегаты с одной сигнатурой — алиасами одного и того же делегата.

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

class Foo { public int _foo; }
class Bar { public int _bar; }

> а не наследовать массивы от IList(T), а потом бросать исключения при вызове половины методов…

Ну, в случае с IList, это не баг, это фича. Имеются соответствующие свойства, которые показывают, какой перед нами IList: read-only, fixed-size или variable-size.

По вашему списку:

    • Just enumeration — IEnumerable˂T˃
    • Read-only, no indexer (.Count) — IReadOnlyCollection˂T˃
    • Variable-size, no indexer (.Add, .Remove, ...) — ICollection˂T˃
    • Read-only, indexable — IReadOnlyList˂T˃
    • Fixed-size, indexable (w/ setter) — см. ниже
    • Variable-size, indexable — IList˂T˃

Итого, нам не хватает одного интерфейса. У нас есть T[], т.е. массив, но он не интерфейс. Ещё IList˂T˃ с проверкой на IsReadOnly, но тоже немного не то. Подробнее тут: stackoverflow.com/q/2110440
Ну, в случае с IList, это не баг, это фича. Имеются соответствующие свойства, которые показывают, какой перед нами IList: read-only, fixed-size или variable-size.

угу, зачем нам вообще ООП, давайте везде пихать if else switch case. Смысл как раз и в том, чтобы приводить к нужному интерфейсу, а дальше с ним работать. Что мешает имплементировать нужные интерфейсы в любом количестве, как это сейчас и бывает?

У нас есть T[], т.е. массив, но он не интерфейс.

Не уловил смысла в этой фразе. T[] от int[] не отличается ничем, кроме того, что тип T нам пока неизвестен.

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

class Foo { public int _foo; }
class Bar { public int _bar; }

в этом есть определенный смысл, но от такого каста пользы было бы много.
> угу, зачем нам вообще ООП, давайте везде пихать if else switch case.

Я вам дал ссылку, прочтите accepted answer. Думаю, он объясняет, почему до .NET 4.5 мы имели то, что имели. Потом разработчики видимо решили, что отдельных интерфейсов для read-only действительно не хватает, и в 4.5 нам дали IReadOnly(Collection|List|Dictionary). Вопрос решён.

> Не уловил смысла в этой фразе.

Смысл в том, что под случай «fixed-size, indexable» нет отдельного интерфейса, который вы, видимо, хотели бы видеть. И я говорю, что хотя обыкновенные массивы — это не интерфейс (например, DI курит в сторонке), T[], переданный методу в качестве fixed-size indexable collection, предоставляет ровно то, что нужно, и интерфейс получается в большинстве случаев не нужен.

> но от такого каста пользы было бы много.

Ну, делегаты — да, чёрт с ними, я сам был бы рад иметь возможность легально приводить всякие ParameterizedThreadStart к Action, пусть и с помощью explicit cast. В конце концов, можем же мы приводить enum к int и даже к другим enum. Но… вот. Может быть, когда-нибудь :)
Зато делегаты можно приводить один к другому вот так:
Del2 del2 = del1.Invoke;
Спасибо. Не могу лайкнуть, так что просто отпишусь так.
Мне очень недостаёт спецификации дженериков.
Сделайте наконец нормальный exception фильтр, чтобы работало

catch (OperationCancelledException)
catch (IOException)
{
}

и non-nullable reference типы. Тогда и этот колхоз с налчеками не нужен будет.

Out-параметры в вызове метода станут не нужны при нормальном PM. ИМХО это важнее любых primary constructor и прочей POCO фигни.
Как вы себе представляете non-nullable reference типы?
Для того, чтобы не было «колхоза с налчеками», уже есть code contracts.

А вот по поводу фильтров исключений я с вами соглашусь, тем более что их поддержка есть в VB.
К C# почему-то их до сих пор не прикрутили.
string! myCoolString = «abc»;
myCoolString = null; — вот тут compile error

А CodeContracts — это яйца в профиль. Если тип не должен содержать Null- это должен проверить компилятор, а не подпорки в рантайме.
string myNotSoCoolString = Foo.GetStringOrNull();
string! myCoolString = myNotSoCoolString; // <- что тут должно быть?

На стадии компиляции запрещать присваивать nullable в не-nullable или в рантайме эксепшн кидать?
Я ж говорю. Компилятор. Разумеется это всё compile-time.

Вы присваиваете несовместимые типы. string и string!..
Сделать каст а-ля
string! myCoolString = (!)myNotSoCoolString; (вот тут рантайм ошибка может свалиться, но это явный каст, а касты это всегда некруто и они всегда могут свалиться)
НУ и такое должно работать
string! myCoolString = myNotSoCoolString ?? «adb»;

То бишь строковые литералы по определению not-null.
Ничем особым не отличается от внезапно вылетевшего NullReferenceException. Точно так же можно пропустить по безалаберности, а потом долго искать, а где это объект стал null.
Вы о чем вообще? Если не юзать касты (а их юзать не нужно в принципе, в C# есть generics которых обычно хватает), то рантайм чеков нет вообще. У вас в принципе не скомпилируется код с возможным null. Приведите нормальный пример где что-то упадёт в рантайме

Единственное всякие штуки а-ля as или аллоцированные массивы в стиле string![] arr = new string![10] не будут работать с non-null, но это мелочи жизни. ДУмаю оно и не нужно будет, не для того non-nullable нужны.
Получаем несуществующий объект из БД через какой-нибудь code first. Или читаем поле такого объекта.
Вы видно не понимаете о чем речь идёт.

Вы в принципе не сможете код написать который возвращает несуществующий объект.
Если хотите продолжить дискуссию — напишите код который делает что вы хотите, и я скажу в каком месте вас компилятор пошлёт лесом.
А лучше просто почитайте статью ниже, и подумайте.
Вы промазали или ивправду считаете что я её не видел :)?

Язык вполне готов к non-nullable типам. Некоторые вещи отвалятся для таких типов, для генериков появятся спецификация на наличие default value, перестанет работать default(T!).
Вопрос цены разумеется открыт. Возможно там человекогоды работы, тут не спорю. Хотя на волне эйфории связанной с Roslyn всё должно быть проще.

Те фичи что описаны в статье на фоне этой элементарны. В них нет вообще ничего ни сложного ни нового.
Хочется верить что Roslyn когда нибудь окупит себя и ребята начнут делать реально крутые штуки
Не промазал, посчитал, что вы и вправду могли не видеть :)
Рад, что я ошибся.
Мне интересно, есть ли от этого хотя бы минимальный какой-то профит, не говоря уж о напыщенном billion dollar заявлении.
Статья во главе имеет две причины своего существования — обилие проверок на null и null-ref ошибки.
Про первое — ситуаций, когда отсутствие объекта есть нормальная ситуация — море, от xml и DB, до проверки user-input, возникает вопрос — в чем разница между проверкой ==null и проверкой .IsDefault или == default(T!)?
А касательно ошибок — взять любой GetBySmth. Ошибка может быть как в том, что аргумент неправильный, так и в том, что объект либо отсутствует, либо алгоритм поиска не может его найти.
Как поможет запрет возвращать null обнаружить или недопустить любую из подобных ошибок, я вот не представляю. Все равно, что запретить человеку болеть.
От каких ошибок это вообще защитит, запретит кому то написать GetByName(null)?
Мне это тоже интересно. Примера, в котором мне понадобились бы non-nullable типы, для себя я придумать так и не смог.

Где-то было мнение, что non-nullable типы — это симметричное дополнение к nullable типам, а язык, мол, стремится к балансу и симметричности. Но если поддержка nullables обусловлена практическими соображениями (DB — этого достаточно), то запрет null — это уже программная логика.

Null может быть и допустимым, и не допустимым значением. Точно так же, как и (int)-1 может иметь смысл (температура в °C), так и не иметь (K). Запрещать/фильтровать значения — это логика. Поэтому лично я не вижу смысла в non-nullable типах.
Запрещать/фильтровать значения — это не обязательно логика, это может быть и контракт. И тогда это удобнее выносить в контракт; а контракты, в свою очередь, лучше иметь на уровне языка, чем на уровне императивных проверок.
Ну да, я под логикой подразумевал и контракты тоже, спасибо за уточнение.
Понимаете ли, тип возвращаемого объекта — это тоже контракт, но нас не удивляет, что он выражен средствами языка. Не очевидно, почему nullability, которое можно выразить силами языка (потенциально), нас должно удивлять.
Самое очевидное применение non-nullable-типов — это методы навроде Person GetPerson(Guid), который по договоренности бросает ошибку, если персона не найдена, и возвращает объект, если найдена. null в возврате быть не может. Это все вынесено в интерфейс и реализуется не нами.

Если у нас есть non-nullability на уровне статического анализатора, то мы можем быть уверены, что GetPerson(Guid).Name гарантированно сработает или кинет ошибку известного типа (персона не найдена), а nullref без объяснений мы не встретим никогда. Если же у нас нет статического анализатора, то мы либо должны проверить, что GetPerson вернул не null и самостоятельно кинуть ошибку «ааа, паника, неопределенное поведение», либо быть готовыми к неспецифицированному nullref.

Статический анализ таких вещей делает код надежнее и читаемее.
Возьмём эту самую функцию, GetPerson, и допустим, что C# поддерживает non-nullable типы. Она выглядела бы как-то так:

Person! GetPerson(Guid guid) { /*...*/ }

Далее введём другую функцию, которая допускает null:

Person FindPerson(Guid guid) { /*...*/ }

И лично я начинаю видеть проблему: Person/Person! означают одну и ту же сущность (некоего человека), но типы оказываются совершенно разными и не всегда взаимозаменяемыми. Это при том, что первоначально разница была лишь в различном поведении (контракте) функций. Не типов! Да, мы можем в своей собственной библиотеке ввести conventions, например, везде использовать только T! GetSomething() (парсер угловые скобки съедает). Но автор другой библиотеки рассудит иначе, и начнётся холивор get vs find, наподобие tabs vs spaces.

А вот контракты сюда подходят идеально.
И лично я начинаю видеть проблему: Person/Person! означают одну и ту же сущность (некоего человека), но типы оказываются совершенно разными и не всегда взаимозаменяемыми.

Это надуманная проблема. Person и Person! соотносятся (должны соотносится при правильном дизайне) так же, как Guid? и Guid — второе неявно приводимо к первому, первое приводимо ко второму через явную предварительную проверку на Null. После этого все последующее ваше рассуждение сводится к (уже существующему) холивару — должно ли быть контрактное ограничение NotNull на возврате GetPerson, или же поведение должно быть как у FindPerson.
> Это надуманная проблема.

Вполне возможно :)
Ну я примерно об этом и написал, оно запретит (ошибкой компиляции) писать Method(null) или return null. Запретит писать FirstOrDefault, и т.п. Но этого и так по факту не происходит, а если происходит, то быстро вылавливается и без запретов компиляции, да и методы GetById они и так самые надежные во всем коде и я и так после них никогда не пишу проверку на null. А в других методах отсутствие объекта это симптом ошибки, а не причина. И сменив тип исключения с nullref на 404 мы не приблизимся ни на шаг к ошибке, разница в затратах на отлов и исправление равна нулю. Другими словами есть сомнения, что код станет надежнее.

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

Если бы не происходило — не было бы постоянных нуллрефов. А они есть.

методы GetById они и так самые надежные во всем коде

Во-первых — нет. Собственно, в сочетании с «запретит писать firstordefault» и напомнило. Типичная эволюция GetPerson в типичном репозитории:

//предположим, что this.Persons - это IQueryable<Persons>, откуда он взялся - не важно
Person GetPerson(Guid id)
{
    return Persons.Single(p => p.Id == id);
}

//комментарии лида: во время исполнения случится Sequence contains no elements, полжизни будем разбираться, что имелось с виду
//(монад у нас предположительно нет, с ними код проще радикально)
Person GetPerson(Guid id)
{
    var person = Persons.SingleOrDefault(p => p.Id == id);
    if (person == null)
        throw new ArgumentOutOfRangeException();
    return person;
}

//лид счастлив
//прошло полгода
//лид сменился
//комментарий нового лида: репозиторий должен всегда возвращать объект или null, проверка на наличие объекта - дело внешнего слоя
//...и переименуйте метод в FindPerson
Person FindPerson(Guid id)
{
    return Persons.SingleOrDefault(p => p.Id == id);
}

//лид счастлив, разработчик счастлив, через неделю падает единственное место кода, где нет проверки на null


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

А во-вторых — есть и другие сценарии, начиная с [DI].Resolve и [ServiceLocator].GetService (знаете, сколько ошибок мы в свое время поймали из-за того, что у них разный контракт?)

я и так после них никогда не пишу проверку на null

Если это внешние по отношению к вашему коду методы, то очень зря, поверьте мне.

А в других методах отсутствие объекта это симптом ошибки, а не причина.

Отсутствие объекта — это всегда симптом, а не причина.

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

Нет. Когда у вас ошибку вылавливает статический анализатор в момент компиляции — это всегда немного надежнее, чем (что угодно) после компиляции. Собственно, пример со статической типизацией уже приводился — можно ведь было сделать динамическую и ловить MemberNotFound, но в C# предпочли статическую. Проверка nullability — это всего лишь один из аспектов статической типизации.

Если не может — очевидно, что он бросает исключение.

Нет. Очевидно, что он не возвращает null. А уж как он это делает — его личное дело. Может бросать exception, может возвращать NullObject, может создавать объект.

лучше уж тогда ввести из джавы фукнциональность throws, данный кейс оно покрыло бы

Не покрыло бы. throws описывает происходящие ошибки, nullability — типы данных. Разные аспекты поведения.

Ну и наконец: не забывайте, что есть еще и входящие параметры, на проверку которых тоже уходит много кода. Если бы оттуда можно было убрать проверки на null — было бы настолько легче…
Мне кажется, что набор тестов + контракты помогут в подавляющем большинстве случаев. Контракты встраиваются в студию и дополняют статическую проверку кода своим статическим анализатором, который вам отловит огромное количество возможных null.

Также можно делать методы расширений для часто используемых операций на объектах, которые могут быть null (например, вызов ивентов). Или написать структуру-враппер для ссылочных типов, если оно настолько нужно.
Мне вот интересно, как вы без null-чеков (они же монада maybe) сделаете получение значения элемента в xml-дереве, где четыре уровня над ним — необязательны. Non-nullable reference types вам тут никак не помогут.
Да, по поводу фильтров. Мне бы гораздо больше пригодились фильтры вида «поймать IOException, даже если он завернут в TargetInvocationException или в DependencyInjectionException»…
Оператор безопасной навигации (Safe Navigation Operator)
Вывод типа для конструктора generic класса

Я джва года такое ждал!

Единственное к чему вопросы — к вот этому неявному объявлению переменных в конструкторе. ИМХО, снижает читабельность кода.

Automatic properties тоже неявно объявляют поля, но ничего, живём :)
Скорее всего, и тут будет своя ниша.
Эх, а мне вот до сих пор не хватает поддержки generic в атрибутах
Непонятно, зачем это:

public Point Move(int dx, int dy) => new Point(X + dx, Y + dy);


Оно отличается от
public Point Move(int dx, int dy){ return new Point(X + dx, Y + dy); }

всего на 6-7 символов, и выглядит не сильно нагляднее. Хотя, поработаем — может быть, почувствуем разницу.
<irony => (Затем, что первый код для сокращения максимальной длины строки можно написать как
public Point Move(int dx, int dy)
  => new Point(X + dx, Y + dy);

Второй же вариант написать как
public Point Move(int dx, int dy)
{ return new Point(X + dx, Y + dy); }
ни как не даст написать IDE из-за своих попыток его отформатировать, да и выглядит такое написание очень странно. Четыре же строчки отводить на такую функцию — не хочется.)
Вывод типов в конструкторе и авто-out — отличные фишки. При работе с легаси-кодом, вполне возможно, понадобится и оператор безопасной навигации. А вот приватные конструкторы и автосвойства, имхо, больше запутывают код, нежели упрощают.
Недавно тоже о таком думал, но все же лучше уже свитчи по Type — автокаст это имхо не так явно для читающего код, как свитч. Свитчи, кстати говоря, несложно реализуются и с тем что есть, могут выглядеть как ниже, fluent-ность можно варьировать при желании.
var @switch = new TypeSwitch()
    .Case<string>(x => ProcessString(x, ...))
    .Case<int>(x => Process(int, ...));
@switch.On(value);
Вы предлагаете какой то лютомедленный и многословный подход (хотя и довольно забавный).
В нем есть аллокация объекта, вызов лямбд, будут замыкания. Это всё ненужно.

Нет ничего более явного чем автокаст внутри is блока. Он подразумевается, но из за ограниченности компилятора всегда делается явно.

У меня полно кода в стиле
if (x is MyClass)
{
vas asMyClass = (MyClass)x;
asMyClass.Foo();
}

Тут одна строчка точно лишняя
Монады вас спасут.

x.As<MyClass>().Do(m => m.Foo());


(ну а «чистый» вариант вашего когда — это as, конечно)
Отличный вариант, жаль не увидел до написания своего коммента ниже.
Делайте
var myClassX = x as MyClass;
if (myClassX  != null) {
    myClassX.Foo();
}

Тоже многословно, но без второго каста.

Вы, кстати говоря, пробовали закодить «лютомедленный» подход? Я пробовал и моя реализация (на словаре экшнов) была лишь в 4 раза медленнее цепочки if-else if. Аллокация там лишь одного объекта на всю конструкцию, лямбды — лишь 1 доп. вызов метода. Вы считаете, что такая довольно редкая конструкция не имеет права быть в 4 раза медленнее для удобства? На C# все-таки не для микроконтроллеров пишут и в большинстве мест можно поступиться скоростью выполнения.

Во многих местах можно заменить такую конструкцию посетителем. В общем и целом, мне кажется что большинству эта фича покажется неважной, и вряд ли станут язык/компилятор усложнять ради ее.
В вашем коде если x == null логика будет отличаться от той что я написал.
А какая логика требуется чаще? Когда null надо отрабатывать, как этот класс, или когда не надо?
Не будет она отличаться — проверка null is something всегда возвращает false — и внутрь if в обоих случаях управление не попадает.
Кстати. Когда полно кода в таком стиле — это что-то с дизайном странное. Не обязательно неправильное, но странное.
Я тут кстати подумал — это вообще невозможно сделать из-за требований обратной совместимости. Допустим, есть код:
class Base {
    public void Foo() {
        Console.WriteLine("Base.Foo");
    }
}
class Derived : Base {
    public new void Foo() {
        Console.WriteLine("Derived.Foo");
    }
}

и в каком-нибудь методе
Base x = new Derived();
if (x is Derived) {
    x.Foo(); // вызываем сокрытый Base.Foo, но только если x - Derived
}

Тогда после введения автокаста эта строчка будет выводить Derived.Foo вместо Base.Foo — изменение уже существующего поведения. Я понимаю, что этот код страшный и не жизненный, но он допустим по спецификации и значит его нельзя ломать. Наличие сокрытия имен в языке делает невозможным существование обратно совместимой реализации того, чего вы хотите.
Кстати, да. Раз уж мы размышляем «а что, если», то представьте себе поведение вот такого кода с вашим добавлением:

interface A
{
    void A();
}

interface B
{
    void B();
}

//...

A x = new Smth();
x.A(); //работает;
if (x is B)
{
    x.B(); //работает
    x.A(); //ошибка компиляции
}


Правда же, прекрасный WTF?
Во первых можно не делать смарткасты для интерфейсов, и оставить дял классов (ввиду отсутствия миксинов).
Во вторых компилятор видит что там x реализует и A и B. И соответственно для первого вызова может сделать каст к B, для второго к A.

И не нужно искать всякие хитрые варианты. Поверьте, я представляю сколько там камней(вроде совпадения методов в разных интерфейсах), но решать их должен Compiler team, а не балаболы на форумах. Проблемы там все детские и решаемые в конце концов компромиссом в виде отключения функциональности для крайних случаев.

Пацаны из котлина давно сделали смарткасты, и никакие Диванные спецы с «прекрасный WTF?» их не остановили. Там нет никакого рокет сайнс.
Во первых можно не делать смарткасты для интерфейсов, и оставить дял классов (ввиду отсутствия миксинов).

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

Поверьте, я представляю сколько там камней(вроде совпадения методов в разных интерфейсах), но решать их должен Compiler team, а не балаболы на форумах.

Вот они их и решили. Я как-то C# Compiler Team доверяю больше, вместе с логикой добавления фич, однажды озвученной Липпертом.
Кстати о логике. Вам не кажется, что примерно половина фич не удовлетворяет правилу -100 очков?
Это не Липпертовская, а Гуннерсоновская логика, афаик. Но нет, не задумывался об этом.
Я и не говорил, что автор — Липперт. Да, про это писал Гуннерсон
В новой версии языка можно будет написать вот так: if (x.CastAs(out Foo foo)) ..., или так: if (x.CastAs<Foo>(out var foo)) ...

Для этого потребуется extension CastAs:
public static T CastAs<T> (this object obj, out T value) {
  if (obj is T) {
    value = (T)obj;
    return true;
  } else {
    value = default(T);
    return false;
  }
}


Я думаю, это вполне нормальная замена актокасту внутри условного оператора, к тому же обратно совместимая.
UPD: кроме того, такая функция, в отличии от автокаста, понравится любителям писать в строчку:
obj.CastAs(out Foo foo) && foo.Parent.CastAs(out Bar bar) && bar.DoSomething();
Зачем же так сложно-то? И зачем новую версию языка ждать? Как уже говорилось выше, прямо сейчас:

x.OfType<Foo>().With(foo => foo.Parent).OfType<Bar>().Do(bar => bar.DoSomething)
Вы правда не видите разницы между этими подходами?..
С первым еще есть хоть какая-то — будет заметно на больших блоках кода (что редко бывает). Со вторым нет никакой.
Ну, второй подход (в строчку) — это лишь дополнительная возможность первого, а никак не основная. Разумеется, такое можно писать и сейчас, разве что производительность будет чуть по-меньше.

Но тот факт, что писать «в строчку» при помощи лямбд можно и сейчас, ни коим образом не отменяет того, что автокаст шарпу не нужен, не так ли?
Но тот факт, что писать «в строчку» при помощи лямбд можно и сейчас, ни коим образом не отменяет того, что автокаст шарпу не нужен, не так ли?

Несомненно.

Просто любителям terse code можно не ждать auto-out.
Насчет первичного конструктора — а он автоматически base вызывает с теми же параметрами, если совпадают?
Или может первичный конструктор наследуется вместе с типом?
При наследовании придется явно указывать параметры для base [предположительно, если первичные конструкторы когда-нибудь будут добавлены в C#]:
public class Point3(int x, int y, int z) : Point(x, y)
{

}
Еще где-то читал, вроде бы на том сайте с наиболее ожидаемыми фичами, про использование выражений (expressions) в атрибутах, интересно, тоже было бы весьма неплохо.
Пропустил «интересно, есть ли такое в планах». Еще хотелось бы генериковые атрибуты.
Sign up to leave a comment.

Articles