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("It is not Point.");
if (GetPoint() is var p) Console.WriteLine($"X={p.X} Y={p.Y}");
else Console.WriteLine("It 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("It is not Point.");
if (GetPoint().Is(out var p) Console.WriteLine("o is Any Type");
else Console.WriteLine("It is not Point.");
На мой взгляд, всё довольно-таки очевидно и удобно.
Но хуже всего то, на мой взгляд, что для компенсации недостатков принятого решения предлагается ввести новый синтаксис!
if (GetPoint() is AnyType p) Console.WriteLine($"X={p.X} Y={p.Y}");
else Console.WriteLine("It is not Point.");
if (GetPoint() is {} p) Console.WriteLine("o is Any Type");
else Console.WriteLine("It 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("It 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("It is not Point.");
if (GetPoint() is { X is int x, Y is int y}) Console.WriteLine($"X={x} Y={y}");
else Console.WriteLine("It 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("It 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("It 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("It 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
Комментарии (46)
aamonster
19.12.2017 23:49Для меня (хотя я и не знаток c#) решение авторов языка довольно очевидно: var — вывод типа. Т.е. в данном случае тип, который может быть возвращён GetPoint — как если бы мы написали var p = GetPoint(). Если GetPoint может вернуть null — значит, p может быть null. Или мы выстрелим себе в ногу.
Makeman Автор
20.12.2017 01:36Такая трактовка тоже имеет место быть, но здесь значение оператора 'var' зависит от контекста.
var p = GetPoint(); // только вывод типа if (GetPoint() is var p) ... // условно эквивалентно if (GetPoint() is null or Point p) ...
Makeman Автор
20.12.2017 01:47К тому говорю, что, на мой взгляд, было бы более очевидно и корректно применить другое имя для оператора, например, назвать его 'any' подразумевая 'null or var' (в классическом значении)
if (GetPoint() is any p) ... if (GetPoint() is null or Point p) ... if (GetPoint() is null or var p) ...
aamonster
20.12.2017 07:43Ровно наоборот. В выражении var p = GetPoint() var должен принимать любое значение, которое может вернуть GetPoint. Так что значение var "прибито гвоздями", нужно что-то типа
if (GetPoint() is nonnull p)
Hazactam
20.12.2017 01:18Еще немного, и Delphi наконец напишут :) С учетом того, что автор языка один, можно особо не удивляться. Только зачем шарп в будущем, если есть Delphi и уже давно? :)
Druu
20.12.2017 02:04> однако по предлагаемым ссылкам значимых аргументов в защиту принятого решения мне найти, к сожалению, так и не удалось, оно просто постулировалось.
Так работает паттерн-матчинг во всех языках. var p — паттерн, который матчит любое значение, аналог «х» в foo x =… в хаскеле, например. Очевидно, что этот паттерн матчится _всегда_. Сделать другое поведение — было бы очень неожиданным для любого человека, знакомого с тем, как работает паттерн-матчинг.
Если вам не нравится синтаксис с var, то как вы предлагаете матчить произвольный паттерн?Deosis
20.12.2017 10:04Тем более такой синтаксис попал в нагрузку с логикой switch:
switch(SomeMethod()) { case int i: return $"int:{i}"; case double d: return $"double: {d}"; case var v: return "Any other"; }
В данном случае вводится переменная внутри выражения.
nicolas2008
20.12.2017 21:19object вместо var было бы логичнее…
Deosis
21.12.2017 08:40Логичнее до тех пор пока метод возвращает object.
Если взять пример:
Rect: Shape
Triangle: Shape
Circle: Shape
то тип переменной будет Shape, а не object.
Makeman Автор
20.12.2017 21:19Предлагаю вместо 'var', у которого уже есть устоявшееся интуитивное значение, использовать другое ключевое слово, например: 'any', 'all', 'let' или хотя бы выражение 'null or var'.
Druu
21.12.2017 07:12Этот паттерн — самый распространенный, так что всякие сложные выражение вроде 'null or var' сразу нет, он должен быть простым и коротким, «дефолтным», так сказать. Матчить произвольное значение — это то, что мы хотим от паттерн-матчинга по умолчанию.
Any и All возможны, но плохо передают суть происходящего, единственный нормальный варинат — это let, но такого ключевого слова в шарпе нету, лучше взять уже имеющееся (var), тем более, что его семантика внутри паттерна полностью совпадает с семантикой вне паттерна.
PsyAfter
20.12.2017 11:08непонятен смысл этих экстеншинов:
public static bool IsNull(this object o) => o is null;
public static bool Is(this T o) => o != null; /* or same 'o is T' */
как объект на котором вызывают метод может быть null?
vadimturkov
20.12.2017 19:22Это фишка extension методов, потому как в реальности метод вызывается не на обьекте, а в статическом классе, который их хранит, а обьект в свою очередь передается лишь как первый параметр.
Lelushak
21.12.2017 02:12Немного дополню ваш ответ для любопытствующих: любой вызов метода от объекта происходит через обращение к сгенерированному для типа статическому классу, в который неявно отдаётся рабочий объект. Разница проявляется на уровне MSIL кода: для статических методов (в том числе extension) используется OP-код «call», который просто вызывает метод без каких-либо проверок, тогда как для обычных методов используется «callvirt» (при условии, что компилятор уверен, что проверка в данном контексте имеет смысл, иначе соптимизирует в call), который сначала проверяет объект на null и при случае выкидывает NullReferenceException.
lair
20.12.2017 11:44В C# 7 незаметным образом, на мой взгляд, просочилась подмена значения оператора 'var', теперь это может значить что-то ещё…
Не таким уж и незаметным: Pattern Matching, раздел "
var
declarations incase
expressions". Единственное, что оттуда не очевидно, это то, что это точно так же применимо к операторуis
.Makeman Автор
20.12.2017 21:30Лично на мой взгляд, стоило ввести новое ключевое слово или добавить какой-то модификатор к 'var', а не просто подменять его значение, в зависимости от контекста использования. Тогда бы никаких неожиданностей не возникало.
lair
20.12.2017 21:43Модификатор — это и есть "контекст использования", так что ничего не меняется. Новое ключевое слово, конечно, круто, но тогда были бы неконсистентные декларации (да и новые ключевые слова — это breaking change).
Makeman Автор
20.12.2017 22:53Сейчас, кажется, всё относительно консистентно, просто 'var' в разных контекстах имеет отличающийся смысл, что не интуитивно и иногда сбивает с толку. На мой взгляд, на консистентность не должно было бы повлиять введение новых ключевых слов для определённых контекстов.
Конечно, если добавлять новые ключевые слова уже сейчас, после релиза, то да — это breaking change. Но до момента релиза это ещё не breaking change :)lair
20.12.2017 22:56-1На мой взгляд, на консистентность не должно было бы повлиять введение новых ключевых слов для определённых контекстов.
Да ну:
Point p; Circle c; var v = new Point(); //... case Point p: case Circle c: case ? v:
Конечно, если добавлять новые ключевые слова уже сейчас, после релиза, то да — это breaking change.
Это когда угодно breaking change.
Makeman Автор
21.12.2017 23:47Не совсем уловил суть примера, поэтому не буду комментировать.
Breaking change — это то, что ломает логику программы при компиляции на следующей весрсии языка (нарушение обратной совместимости). Можно добавить хоть десяток новых ключевых слов, но если старый код компилируется одинаково успешно, как на новой, так и на старой версиях языка, то никакого breaking change в классическом понимании не произошло. Просто расширился синтаксис и функционал, добавились новые ключевые слова и допустимые выражения с ними.lair
22.12.2017 00:19Breaking change — это то, что ломает логику программы при компиляции на следующей весрсии языка (нарушение обратной совместимости).
Ну да. Добавление нового ключевого слова именно таким и будет. Вот допустим, решили вы добавить ключевое слово
any
вместоvar
. Чтобы, значит, былоis any x
. Представьте на мгновение, что кто-то из разработчиков объявил тип, названныйany
. Что будет, когда он перекомпилирует свой код под новой версией языка? Правильно, смена поведения (даже не ошибка компиляции).Makeman Автор
22.12.2017 01:02Сейчас уже да, верно, но если бы 'any' было введено вместе с текущей реализацией pattern-matching'а, то синтаксис 'is any x' [где any — тип] был бы невалиден до того, поэтому введение ключевого слова в контексте нового синтаксиса ничего бы не поломало. По крайней мере, я не могу придумать пример, где бы это ещё могло проявиться.
Makeman Автор
22.12.2017 01:05P.S. Выражение 'o is any' можно было бы расценивать в старом значении [проверка типа], но новое 'o is any x' уже означало бы иное поведение.
lair
22.12.2017 01:06Вот это как раз была бы адская неоднозначность.
Makeman Автор
22.12.2017 01:20Почему же неоднозначность? Ключевое слово 'any' без 'x' употребляться не может само по себе.
Более того, ради интереса сейчас проверил, можно ли объявить класс с именем 'var'. Можно! Всё компилируется, вот только 'var' в обычном значении перестаёт работать, поскольку воспринимается как тип, а ведь когда-то же и 'var' не было в C#.lair
22.12.2017 11:30Почему же неоднозначность? Ключевое слово 'any' без 'x' употребляться не может само по себе.
Потому что стоит забыть переменную и поведение радикально меняется.
ведь когда-то же и 'var' не было в C#.
Ну так это и был breaking change.
Makeman Автор
22.12.2017 18:19Не спорю с тем, что поведение радикально меняется, но старый код (без изменений) будет работать по-прежнему, поэтому формально это не breaking change, обратная совместимость сохранена.
Конечно, для разработчика нововведение может выглядеть ужасно непредсказуемо и даже ломать его логиченые и интуитивные мыслительные шаблоны, как произошло у меня с 'var', но, скорее, это можно назвать pattern breaking. :)
lair
22.12.2017 01:06По крайней мере, я не могу придумать пример, где бы это ещё могло проявиться.
Если я ничего не путаю, все другие места с этим ключевым словом все равно потребуют эскейпинга.
Sinatr
20.12.2017 12:12Интересно. Не использую пока 7.0, но судя по всему тема уже давно разжеванна (то, что вы пользуетесь if вместо switch/case, значения не имеет). У null нет типа, var может быть чем угодно, включая null, следовательно pattern-matching c var всегда возможен. Думайте об «is var» как о default в switch/case.
Это не unexpected, а скорее «невыученный» behavior. В любом языке их полно, маленькие и большие нюансы, любой из которых unexpected для новичка. C нетерпением жду перехода нашего отдела на VS 2017 и свежих граблей!
P.S.: is any — интересная идея, но, как вы понимаете, запоздалая, т.к. is var уже используют и вряд ли кто-то обрадуется несовместимости нового C#.dmitry_dvm
20.12.2017 12:38is Type гарантирует, что там не null, а is var ничего не гарантирует. Получается, что is var работает, как as Type. А нафига?
lair
20.12.2017 12:56Получается, что is var работает, как as Type. А нафига?
Для общности с
case var x
.
Druu
20.12.2017 16:30> Получается, что is var работает, как as Type. А нафига?
Он нужен для матчинга произвольного аргумента в подпаттернах.
jack128
20.12.2017 18:27в идеале (то есть не обращая внимания на совместимость) и имея на уме not null reference из C# 8 имеет смысл такой синтаксис:
if (MyMethod() is var x) // x is not null
if (MyMethod() is var? x) // x can be null
Makeman Автор
20.12.2017 21:40На мой взгляд, довольно очевидный вариант в контексте C# 8 может выглядеть так
if (GetPoint() is Point p) // всегда true if (GetPoint() is Point? p) // true/false if (GetPoint() is var p) // true/false, если метод возвращает Point?, только true, если Point if (GetPoint() is null or Point p) // всегда true или неприминимо if (GetPoint() is null or Point? p) // всегда true if (GetPoint() is any p) // всегда true
dmitry_dvm
Оператор is однозначно говорит, о том, что объект является таким-то типом. Что говорит is var? Не понимаю зачем это нужно вообще было делать.
Makeman Автор
Лично для меня на интуитивном уровне это что-то вроде деконструкции метода 'Is' со следующей имплементацией (для декларации переменных в самом выражении):
Но, увы, архитекторы языка посчитали иначе.
Makeman Автор
Уточнение:
По моей логике в выражении
можно применить вывод типа и получить
Нона самом деле это не эквивалентная замена в C# 7.
Festelo
Вывод типа можно применить не везде, а когда можно, то зачем тогда вообще is?
Makeman Автор
Для 'bar' будет выведен тип 'object' (поскольку метод возвращает 'object').
if (p is Point pp) — имеет некоторый смысл, можно не декларировать переменную отдельно. Это очень удобно для однострочных методов [bodied members].
Иначе пришлось бы писать только так
xorza
мне непонятно, как
это вообще может скомпилится? Компилятор должен вывести типы для Is и для out var p, но он не сможет это сделать.
— на мой взгляд тоже не должно компилится по тем же причинам, что не компилится
Makeman Автор
Метод 'GetPoint()' возвращает 'Point' либо 'null', поэтому компилятор может вывести тип по сигнатуре метода.
В C# 7 добавлена новая фишка, вместо
можно теперь писать так
Поэтому следующее выражение компилируется в C# 7
Однако следующие выражения уже относятся к другой новой фишке — pattern-matching, но и они успешно компилируются