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

в 16:10, , рубрики: .net, C#

В языке 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<T>(this object o) => o is T;
    public static bool Is<T>(this T o) => o != null; /* or same 'o is T' */
    public static bool Is<T>(this T o, out T x) => (x = o) != null; /* or same '(x = o) is T' */
    /* .... */

    public static T As<T>(this object o) where T : class => o as T;
    public static T Of<T>(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($"X={p.X} Y={p.Y}");
else Console.WriteLine("There is not point.");
// but
if (GetPoint() is var 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. Также вы можете выразить своё мнение по ряду других предложений:

Автор: Makeman

Источник


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js