В языке 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. Также вы можете выразить своё мнение по ряду других предложений:
- 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
Автор: Makeman