Исправлять ли unexpected behavior в C# 7 или оставить как есть, усложнив синтаксис для компенсации?

habr.png

В языке C# с давних времён есть оператор 'is' назначение которого довольно ясное

if (p is Point) Console.WriteLine("p is Point");
else Console.WriteLine("p is not Point or null");


Кроме того его можно использовать для проверок на null

if (p is object) Console.WriteLine("p is not null");
if (p is null) Console.WriteLine("p is null");


В C# 7 анонсирована новая возможность pattern-matching

if (GetPoint() is Point p) Console.WriteLine($"X={p.X} Y={p.Y}");
else Console.WriteLine("There is not point.");

if (GetPoint() is var p) Console.WriteLine($"X={p.X} Y={p.Y}");
else Console.WriteLine("There is not point.");


Вопрос, что произойдёт в обоих случаях, если метод вернёт 'null'? Вы уверены?

Возможно, вы уже сталкивались с этой странной особенностью языка, поэтому она не окажется для вас сюрпризом, но недавно я был крайне удивлён (спасибо JetBrains за подсказку!) тем, что выражение 'GetPoint () is var p' всегда истинно, а 'GetPoint () is AnyType p' нет.

Всегда считал 'var' неким белым ящиком, который позволяет не указывать тип переменной явно, если её он может быть выведен компилятором [type inference].

В C# 7 незаметным образом, на мой взгляд, просочилась подмена значения оператора 'var', теперь это может значить что-то ещё…

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

Но можно ли бы было сделать лучше? Взгляните.

public static class LanguageExtensions
{
    public static bool IsNull(this object o) => o is null;
    public static bool Is(this object o) => o is T;
    public static bool Is(this T o) => o != null; /* or same 'o is T' */
    public static bool Is(this T o, out T x) => (x = o) != null; /* or same '(x = o) is T' */
    /* .... */

    public static T As(this object o) where T : class => o as T;
    public static T Of(this object o) => (T) o;
}

public Point GetPoint() => null; // new Point { X = 123, Y = 321 };

if (GetPoint().Is(out AnyType p) Console.WriteLine($"X={p.X} Y={p.Y}");
else Console.WriteLine("There is not point.");

if (GetPoint().Is(out var p) Console.WriteLine("o is Any Type");
else Console.WriteLine("There is not point.");


На мой взгляд, всё довольно-таки очевидно и удобно.

Но хуже всего то, что для компенсации недостатков принятого решения предлагается ввести новый синтаксис!

if (GetPoint() is AnyType p) Console.WriteLine($"X={p.X} Y={p.Y}");
else Console.WriteLine("There is not point.");

if (GetPoint() is {} p) Console.WriteLine("o is Any Type");
else Console.WriteLine("There is not point.");

if (GetPoint() is var p) Console.WriteLine("Always true");


Более того это влияет на синтаксис дальнейшей, ещё не анонсированной, возможности рекурсивного pattern-matching.

Могло бы быть

if (GetPoint() is AnyType p { X is int x, Y is int y}) Console.WriteLine($"X={x} Y={y}");
else Console.WriteLine("There is not point.");

if (GetPoint() is var p { X is int x, Y is int y}) Console.WriteLine($"X={x} Y={y}");
else Console.WriteLine("There is not point.");

if (GetPoint() is { X is int x, Y is int y}) Console.WriteLine($"X={x} Y={y}");
else Console.WriteLine("There is not point.");


Но предполагается

if (GetPoint() is AnyType { X is int x, Y is int y} p) Console.WriteLine($"X={p.X} Y={p.Y}");
else Console.WriteLine("There is not point.");

if (GetPoint() is var { X is int x, Y is int y} p) Console.WriteLine("Always true");

if (GetPoint() is { X is int x, Y is int y} p) Console.WriteLine($"X={p.X} Y={p.Y}");
else Console.WriteLine("There is not point.");


С моей точки зрения, всё выглядит сикось-накось, грядёт очередное «расширение» понятия для блока кода '{ }'.

Но теперь мы подходим к главной проблеме — всегда истинное выражение 'x is var y' уже в релизе, поэтому изменение его поведения является breaking change, на которое пойти теперь почти невозможно по мнению ребят из репозитория.

Очень хорошо понимаю их опасения, но как разработчик, стремящийся к чистоте кода, я готов смириться с даже таким breaking change, ради чистого и ясного синтаксиса языка.

Более того, данное исправление можно произвести наиболее мягко в контексте грядущего функционала для C# 8 Null Reference Types. Например, у нас есть метод

public bool SureThatAlwaysTrue(AnyType item) => item is var x;


Если его скомпилировать в C# 8, но уже с тем условием, что выражение может быть 'false', если 'item == null', то поведение метода не изменится, поскольку в контексте C# 8 выражение 'AnyType item' предполагает, что 'item!= null' (компилятор не пропускает выражение 'SureThatAlwaysTrue (null)' и отображает warning message в случае 'SureThatAlwaysTrue (null)'). Сообщение можно лишь намеренно убрать с помощью оператора '!' следующим образом 'SureThatAlwaysTrue (null!)' или же переписать метод так

public bool SureThatAlwaysTrue(AnyType? item) => item is var x;


Проблема breaking change остаётся лишь для Nullable Value Types, которые уже присутствуют в C# 7

public bool SureThatAlwaysTrue(int? item) => item is var x;


Такой метод даже при наличии warning message нужно будет отрефакторить вручную [breaking change].

Все ключевые моменты я рассказал максимально честно, как сам их понимаю и вижу, поэтому теперь очень интересует ваше мнение как разработчиков: предпочитаете вы всё оставить как есть и мириться в дальнейшем с усложнённым синтаксисом или же готовы принять не столь уж и масштабное breaking change ради сохранения чистоты и ясности языка?

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

Для ознакомления:
Question: what does 'var' mean?

Голосовать «за» или «против» следует ниже по ссылке с более детальными предложениями по улучшению синтаксиса языка:
Pattern-matching rethinking (at C# 8 Nullable Reference Types context)

P.S. Также вы можете выразить своё мнение по ряду других предложений:

  • Allow to use single control flow statements into expression bodied members
  • Allow type inference for class members with autoinitializers and methods (use «var»/«auto» keywords)
  • Add operator «of» for right-side type casting to avoid »(item as Type).Member» anti-pattern and round bracket hell in some cases

© Habrahabr.ru