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

Комментарии (46)


  1. dmitry_dvm
    19.12.2017 22:26

    Оператор is однозначно говорит, о том, что объект является таким-то типом. Что говорит is var? Не понимаю зачем это нужно вообще было делать.


    1. Makeman Автор
      19.12.2017 22:37

      Лично для меня на интуитивном уровне это что-то вроде деконструкции метода 'Is' со следующей имплементацией (для декларации переменных в самом выражении):

      public static bool Is<T>(this T o, out T x) => (x = o) != null; /* or same '(x = o) is T' */
      
      if (GetPoint().Is(out var p) Console.WriteLine("o is Any Type");
      else Console.WriteLine("There is not point.");
      
      if (GetPoint() is var p) ...
      


      Но, увы, архитекторы языка посчитали иначе.


      1. Makeman Автор
        19.12.2017 22:42

        Уточнение:

        public Point GetPoint() => ...

        По моей логике в выражении
        if (GetPoint() is Point p) ...

        можно применить вывод типа и получить
        if (GetPoint() is var p) ...

        Нона самом деле это не эквивалентная замена в C# 7.


        1. Festelo
          20.12.2017 01:48

          Вывод типа можно применить не везде, а когда можно, то зачем тогда вообще is?

          object Foo()  => rnd.Next(1) == 1 ? 123 : "456";
          if (Foo(true) is string str) {}
          if (Foo(true) is var bar) {} // Какого типа должна быть переменная bar?
          
          Point GetPoint() => new Point();
          var p = GetPoint();
          if (p is Point pp) {} // Не имеет смысла, т.к. GetPoint() возвращает всегда только Point
          if (p is var pp) {} //  
          


          1. Makeman Автор
            20.12.2017 02:01

            Для 'bar' будет выведен тип 'object' (поскольку метод возвращает 'object').

            if (p is Point pp) — имеет некоторый смысл, можно не декларировать переменную отдельно. Это очень удобно для однострочных методов [bodied members].

            public bool IsValid() => GetPoint() is Point p ? Validate(p.X, p.Y) : false;

            Иначе пришлось бы писать только так

            public bool IsValid()
            {
                var p = GetPoint();
                return p is Point ? Validate(p.X, p.Y) : false; 
                // 'p is Point' в нашем случае можно равноценно заменить на 'p != null'
            }


      1. xorza
        20.12.2017 19:13

        мне непонятно, как

        GetPoint().Is(out var p)

        это вообще может скомпилится? Компилятор должен вывести типы для Is и для out var p, но он не сможет это сделать.
        GetPoint() is var p
        — на мой взгляд тоже не должно компилится по тем же причинам, что не компилится
        Point p = GetPoint(); if(p is var) {}


        1. Makeman Автор
          20.12.2017 19:21

          Метод 'GetPoint()' возвращает 'Point' либо 'null', поэтому компилятор может вывести тип по сигнатуре метода.

          В C# 7 добавлена новая фишка, вместо

          int i;
          var i = 0;
          int.TryParse("123", out i)

          можно теперь писать так
          int.TryParse("123", out int i)
          int.TryParse("123", out var i)


          Поэтому следующее выражение компилируется в C# 7
          GetPoint().Is(out var p)


          Однако следующие выражения уже относятся к другой новой фишке — pattern-matching, но и они успешно компилируются
          if (GetPoint() is Point p)
          if (GetPoint() is var p)



  1. aamonster
    19.12.2017 23:49

    Для меня (хотя я и не знаток c#) решение авторов языка довольно очевидно: var — вывод типа. Т.е. в данном случае тип, который может быть возвращён GetPoint — как если бы мы написали var p = GetPoint(). Если GetPoint может вернуть null — значит, p может быть null. Или мы выстрелим себе в ногу.


    1. Makeman Автор
      20.12.2017 01:36

      Такая трактовка тоже имеет место быть, но здесь значение оператора 'var' зависит от контекста.

      var p = GetPoint(); // только вывод типа
      if (GetPoint() is var p) ...
      // условно эквивалентно 
      if (GetPoint() is null or Point p) ...
      


      1. 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) ...
        



        1. aamonster
          20.12.2017 07:43

          Ровно наоборот. В выражении var p = GetPoint() var должен принимать любое значение, которое может вернуть GetPoint. Так что значение var "прибито гвоздями", нужно что-то типа


          if (GetPoint() is nonnull p) 


  1. Hazactam
    20.12.2017 01:18

    Еще немного, и Delphi наконец напишут :) С учетом того, что автор языка один, можно особо не удивляться. Только зачем шарп в будущем, если есть Delphi и уже давно? :)


  1. Gentlee
    20.12.2017 01:41

    «There is not point.» — товарищ Makeman, Вы очень плохо знаете английский язык, пожалуйста, пользуйтесь в следующий раз хотя бы google translate.


    1. Makeman Автор
      20.12.2017 01:42

      Верно, спасибо! Я не очень хорошо знаю английский. Исправил на «There is null.», так будет корректно?


      1. fedorro
        20.12.2017 13:37

        Возможно предлагалось использовать It вместо There (It is not point., It is null.).


  1. Druu
    20.12.2017 02:04

    > однако по предлагаемым ссылкам значимых аргументов в защиту принятого решения мне найти, к сожалению, так и не удалось, оно просто постулировалось.

    Так работает паттерн-матчинг во всех языках. var p — паттерн, который матчит любое значение, аналог «х» в foo x =… в хаскеле, например. Очевидно, что этот паттерн матчится _всегда_. Сделать другое поведение — было бы очень неожиданным для любого человека, знакомого с тем, как работает паттерн-матчинг.

    Если вам не нравится синтаксис с var, то как вы предлагаете матчить произвольный паттерн?


    1. 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";
      }

      В данном случае вводится переменная внутри выражения.


      1. nicolas2008
        20.12.2017 21:19

        object вместо var было бы логичнее…


        1. Deosis
          21.12.2017 08:40

          Логичнее до тех пор пока метод возвращает object.
          Если взять пример:
          Rect: Shape
          Triangle: Shape
          Circle: Shape
          то тип переменной будет Shape, а не object.


        1. lair
          21.12.2017 11:40

          … и получить боксинг на пустом месте?


    1. Makeman Автор
      20.12.2017 21:19

      Предлагаю вместо 'var', у которого уже есть устоявшееся интуитивное значение, использовать другое ключевое слово, например: 'any', 'all', 'let' или хотя бы выражение 'null or var'.


      1. Druu
        21.12.2017 07:12

        Этот паттерн — самый распространенный, так что всякие сложные выражение вроде 'null or var' сразу нет, он должен быть простым и коротким, «дефолтным», так сказать. Матчить произвольное значение — это то, что мы хотим от паттерн-матчинга по умолчанию.
        Any и All возможны, но плохо передают суть происходящего, единственный нормальный варинат — это let, но такого ключевого слова в шарпе нету, лучше взять уже имеющееся (var), тем более, что его семантика внутри паттерна полностью совпадает с семантикой вне паттерна.


  1. 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?


    1. Lelushak
      20.12.2017 13:21

      Статические методы можно вызывать на объекте, который равен null.


    1. vadimturkov
      20.12.2017 19:22

      Это фишка extension методов, потому как в реальности метод вызывается не на обьекте, а в статическом классе, который их хранит, а обьект в свою очередь передается лишь как первый параметр.


      1. Lelushak
        21.12.2017 02:12

        Немного дополню ваш ответ для любопытствующих: любой вызов метода от объекта происходит через обращение к сгенерированному для типа статическому классу, в который неявно отдаётся рабочий объект. Разница проявляется на уровне MSIL кода: для статических методов (в том числе extension) используется OP-код «call», который просто вызывает метод без каких-либо проверок, тогда как для обычных методов используется «callvirt» (при условии, что компилятор уверен, что проверка в данном контексте имеет смысл, иначе соптимизирует в call), который сначала проверяет объект на null и при случае выкидывает NullReferenceException.


  1. lair
    20.12.2017 11:44

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

    Не таким уж и незаметным: Pattern Matching, раздел "var declarations in case expressions". Единственное, что оттуда не очевидно, это то, что это точно так же применимо к оператору is.


    1. Makeman Автор
      20.12.2017 21:30

      Лично на мой взгляд, стоило ввести новое ключевое слово или добавить какой-то модификатор к 'var', а не просто подменять его значение, в зависимости от контекста использования. Тогда бы никаких неожиданностей не возникало.


      1. lair
        20.12.2017 21:43

        Модификатор — это и есть "контекст использования", так что ничего не меняется. Новое ключевое слово, конечно, круто, но тогда были бы неконсистентные декларации (да и новые ключевые слова — это breaking change).


        1. Makeman Автор
          20.12.2017 22:53

          Сейчас, кажется, всё относительно консистентно, просто 'var' в разных контекстах имеет отличающийся смысл, что не интуитивно и иногда сбивает с толку. На мой взгляд, на консистентность не должно было бы повлиять введение новых ключевых слов для определённых контекстов.

          Конечно, если добавлять новые ключевые слова уже сейчас, после релиза, то да — это breaking change. Но до момента релиза это ещё не breaking change :)


          1. 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.


            1. Makeman Автор
              21.12.2017 23:47

              Не совсем уловил суть примера, поэтому не буду комментировать.

              Breaking change — это то, что ломает логику программы при компиляции на следующей весрсии языка (нарушение обратной совместимости). Можно добавить хоть десяток новых ключевых слов, но если старый код компилируется одинаково успешно, как на новой, так и на старой версиях языка, то никакого breaking change в классическом понимании не произошло. Просто расширился синтаксис и функционал, добавились новые ключевые слова и допустимые выражения с ними.


              1. lair
                22.12.2017 00:19

                Breaking change — это то, что ломает логику программы при компиляции на следующей весрсии языка (нарушение обратной совместимости).

                Ну да. Добавление нового ключевого слова именно таким и будет. Вот допустим, решили вы добавить ключевое слово any вместо var. Чтобы, значит, было is any x. Представьте на мгновение, что кто-то из разработчиков объявил тип, названный any. Что будет, когда он перекомпилирует свой код под новой версией языка? Правильно, смена поведения (даже не ошибка компиляции).


                1. Makeman Автор
                  22.12.2017 01:02

                  Сейчас уже да, верно, но если бы 'any' было введено вместе с текущей реализацией pattern-matching'а, то синтаксис 'is any x' [где any — тип] был бы невалиден до того, поэтому введение ключевого слова в контексте нового синтаксиса ничего бы не поломало. По крайней мере, я не могу придумать пример, где бы это ещё могло проявиться.


                  1. Makeman Автор
                    22.12.2017 01:05

                    P.S. Выражение 'o is any' можно было бы расценивать в старом значении [проверка типа], но новое 'o is any x' уже означало бы иное поведение.


                    1. lair
                      22.12.2017 01:06

                      Вот это как раз была бы адская неоднозначность.


                      1. Makeman Автор
                        22.12.2017 01:20

                        Почему же неоднозначность? Ключевое слово 'any' без 'x' употребляться не может само по себе.

                        Более того, ради интереса сейчас проверил, можно ли объявить класс с именем 'var'. Можно! Всё компилируется, вот только 'var' в обычном значении перестаёт работать, поскольку воспринимается как тип, а ведь когда-то же и 'var' не было в C#.


                        1. lair
                          22.12.2017 11:30

                          Почему же неоднозначность? Ключевое слово 'any' без 'x' употребляться не может само по себе.

                          Потому что стоит забыть переменную и поведение радикально меняется.


                          ведь когда-то же и 'var' не было в C#.

                          Ну так это и был breaking change.


                          1. Makeman Автор
                            22.12.2017 18:19

                            Не спорю с тем, что поведение радикально меняется, но старый код (без изменений) будет работать по-прежнему, поэтому формально это не breaking change, обратная совместимость сохранена.

                            Конечно, для разработчика нововведение может выглядеть ужасно непредсказуемо и даже ломать его логиченые и интуитивные мыслительные шаблоны, как произошло у меня с 'var', но, скорее, это можно назвать pattern breaking. :)


                  1. lair
                    22.12.2017 01:06

                    По крайней мере, я не могу придумать пример, где бы это ещё могло проявиться.

                    Если я ничего не путаю, все другие места с этим ключевым словом все равно потребуют эскейпинга.


  1. 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#.


    1. dmitry_dvm
      20.12.2017 12:38

      is Type гарантирует, что там не null, а is var ничего не гарантирует. Получается, что is var работает, как as Type. А нафига?


      1. lair
        20.12.2017 12:56

        Получается, что is var работает, как as Type. А нафига?

        Для общности с case var x.


      1. Druu
        20.12.2017 16:30

        > Получается, что is var работает, как as Type. А нафига?

        Он нужен для матчинга произвольного аргумента в подпаттернах.


  1. 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


    1. 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