Занимательный C#

    Занимательный C#

    Для оценки качества диагностик анализатора C# кода PVS-Studio мы проверяем большое количество различных проектов. Т.к. проекты пишутся разными людьми в различных командах в разных компаниях, нам приходится сталкиваться с различными стилями, сокращениями, да и просто возможностями, которые предлагает язык C# программистам. В этой статье я хочу обзорно пройтись по некоторым моментам, которые предлагает нам замечательный язык C#, и по тем проблемам, на которые можно наткнуться при его использовании.

    Picture 1


    Ремарка.
    Данная статья больше ориентирована на любознательность и описывает те вещи, про которые мне лично показалось интересным рассказать.

    Свойства и что с ними можно делать


    Мы все знаем, что свойства — это пара функций: аксессор и мутатор, для изменения и чтения значения в каком-то поле. Ну, или по крайней мере так было до версии языка C# 3.0. Т.е. классически они должны выглядеть вот так:
    class A
    {
      int index;
      public int Index
      {
        get { return index; }
        set { index = value; }
      }
    }

    Шли годы, и стандарты языка, и свойства обросли разными возможностями.

    Начнем понемногу. В стандарте C# 3.0 появилась всем известная возможность опустить поле, т.е. записать так:
    class A
    {
      public int Index { get; set; }
    }

    В C# 6.0 пошли еще дальше и позволили убрать «set».
    class A
    {
      public int Index { get; }
    }

    Так писать можно было и до C# 6.0, но записать в такую переменную что-либо было нельзя. Теперь это, по факту, является аналогом readonly полей, т.е. задавать значение таких свойств можно только в конструкторе.

    Свойства и поля можно инициализировать различными способами. Например, так:
    class A
    {
      public List<int> Numbers { get; } = new List<int>(); 
    }

    Ну или так:
    class A
    {
      public List<int> Numbers = new List<int>();
    }

    А еще можно написать так:
    class A
    {
      public List<int> Numbers => new List<int>();
    }

    И в последнем случае вас будет ждать неприятный сюрприз. На самом деле, в последнем примере вы создали вот такое вот свойство:
    class A
    {
      public List<int> Numbers { get { return new List<int>(); } }
    }

    Т.е. когда вы попытаетесь заполнить Numbers, то у вас ничего не получится в принципе, каждый раз вы будете иметь новый список.
    A a = new A();
    a.Numbers.Add(10);
    a.Numbers.Add(20);
    a.Numbers.Add(30);

    Будьте внимательны, когда сокращаете запись, иногда это может привести к весьма долгому поиску ошибки.

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

    Следующий код прекрасно компилируется и даже работает.
    class A
    {
      int index;
      public int Index
      {
        get { return index; }
        set { 
          value = 20; 
          index = value; }
      }
    }
    static void Main(string[] args)
    {
      A a = new A();
      a.Index = 10;
      Console.WriteLine(a.Index);
    }

    Результатом работы будет вывод числа «20», а никак не «10».

    Казалось бы, зачем кому-то сдалось записывать значение 20 в value? Оказывается, даже в этом может быть смыл. Но для пояснения этого смысла, мы немного отвлекаемся от свойств и расскажем о ключевом символе @. Данный ключевой символ позволяет создавать переменные схожие по написаю с ключевыми словами. Например: @this, @operator и т.д. Но никто не запрещает, данный символ, пихать куда душа пожелает, например:
    class A
    {
      public int index;
      public void CopyIndex(A @this)
      {
        this.@index = @this.index;
      }
    }
    static void Main(string[] args)
    {
      A a = new A();
      @a.@index = 10;
      a.@CopyIndex(new A() { @index = 20 });
      Console.WriteLine(a.index);
    }

    Результатом работы, как всегда в этой статье, будет вывод числа «20», а никак не «10».

    На самом деле, символ @ необходим только в одном месте, когда пишем имя параметра @this в функции CopyIndex. В других местах это просто лишний код, который, к тому же, затрудняет понимание написанного.

    С этими знаниями вернемся к свойствам и предположим, что у нас есть следующий класс:
    class A
    {
      int value;
      public int Value
      {
        get { return @value; }
        set { @value = value; }
      }
      public A()
      {
        value = 5;
      }
    }

    Можно подумать, что в свойстве Value изменится поле value класса A. Но, на самом деле, так не произойдет, и результатом работы следующий программы будет 5, а не 10.
    static void Main(string[] args)
    {
      A a = new A();
      a.Value = 10;
      Console.WriteLine(a.Value);
    }

    Данное поведение происходит из-за не соответствия @value в get и @value в set. @value в get будет являться ничем иным, кроме как, полем класса A. А @value в set на самом деле — это параметр функции set. Таким образом мы просто пишем value само в себя и никак не затрагиваем поле value в классе А.

    Инициализация коллекций


    Для начала вспомним различные способы инициализации массивов:
    string[] test1 = new string[] { "1", "2", "3" };
    string[] test2 = new[] { "1", "2", "3" };
    string[] test3 = { "1", "2", "3" };
    string[,] test4 = { { "11", "12" }, 
                        { "21", "22" }, 
                        { "31", "32" } };

    Со списками дела обстоят легче и есть только один вариант инициализации:
    List<string> test2 = new List<string>(){ "1", "2", "3" };

    Ну и напоследок Dictionary:
    Dictionary<string, int> test = 
      new Dictionary<string, int>() { { "a-a", 1 }, 
                                      { "b-b", 2 }, 
                                      { "c-c", 3 } };

    А вот ради следующего способа я и писал данный раздел, ибо его я видел впервые:
    Dictionary<string, int> test = 
      new Dictionary<string, int>() { 
        ["a-a"] = 1,
        ["b-b"] = 2,
        ["c-c"] = 3
      };

    Немного о LINQ запросах


    LINQ запросы в принципе сама по себе вещь удобная. Собираем цепочку с необходимыми выборками и на выходе получаем необходимую информацию. Для начала опишем пару приятных моментов, которые могут не прийти в голову, пока сам не увидишь их. Для начала рассмотрим базовый пример:
    void Foo(List<int> numbers1, List<int> numbers2) {
      var selection1 = numbers1.Where(index => index > 10);
      var selection2 = numbers2.Where(index => index > 10);
    }

    Нетрудно заметить, что в выше описанном примере есть несколько одинаковых проверок. То есть по-хорошему, их можно вынести в отдельную «функцию»:
    void Foo(List<int> numbers1, List<int> numbers2) {
      Func<int, bool> whereFunc = index => index > 10;
      var selection1 = numbers1.Where(index => whereFunc(index));
      var selection2 = numbers2.Where(index => whereFunc(index));
    }

    Уже стало лучше, если функции большие, то вообще прекрасно. Немного смущает вызов whereFunc: какой-то он неказистый. На самом деле, это тоже не проблема:
    void Foo(List<int> numbers1, List<int> numbers2) {
      Func<int, bool> whereFunc = index => index > 10;
      var selection1 = numbers1.Where(whereFunc);
      var selection2 = numbers2.Where(whereFunc);
    }

    Вот теперь и лаконично и опрятно.

    Теперь немного о нюансах работы LINQ выражений. Например, строчка кода не приведет к моментальной выборке данных из коллекции numbers1.
    IEnumerable<int> selection = numbers1.Where(whereFunc);

    Выборка данных начнется, только когда будет выполнена конвертация последовательности в коллекцию List<int>:
    List<int> listNumbers = selection.ToList();

    Этот нюанс работы может легко привести к использованию захваченной переменной уже после того, как её значение изменилось. Возьмем простой пример. Допустим, нам нужна функция Foo, которая вернет из массива "{ 1, 2, 3, 4, 5 }" только те элементы, численные значения которых меньше индекса элемента, т.е:
    0 :
    1 :
    2 : 1
    3 : 1, 2
    4 : 1, 2, 3

    Её сигнатура пусть будет такой:
    static Dictionary<int, IEnumerable<int>> Foo(int[] numbers)
    { ....  }

    А вызов вот такой:
    foreach (KeyValuePair<int, IEnumerable<int>> subArray in 
               Foo(new[] { 1, 2, 3, 4, 5 }))
    Console.WriteLine(string.Format("{0} : {1}", 
                      subArray.Key, 
                      string.Join(", ", subArray.Value)));

    Всё вроде бы просто. Теперь напишем саму реализацию на основе LINQ. Она будет выглядеть вот так:
    static Dictionary<int, IEnumerable<int>> Foo(int[] numbers)
    {
      var result = new Dictionary<int, IEnumerable<int>>();
      for (int i = 0; i < numbers.Length; i++)
        result[i] = numbers.Where(index => index < i);
      return result;
    }

    Как можно видеть, всё предельно просто. Мы берем и поочерёдно «создаем» выборки из массива numbers.

    Результатом работы такой программы будет вот такой текст в консоли:
    0 : 1, 2, 3, 4
    1 : 1, 2, 3, 4
    2 : 1, 2, 3, 4
    3 : 1, 2, 3, 4
    4 : 1, 2, 3, 4

    Проблема тут как раз в замыкании, которое произошло в лямбде index => index < i. Переменная i была захвачена, но, так как вызов лямбда выражения index => index < i не происходил до момента, когда мы попросили результат в функции string.Join(", ", subArray.Value), значение в ней было не такое, как в момент формирования LINQ запроса. Во время получения данных из выборки значения i было равным 5, что привело к неверному результату вывода.

    Недокументированные костыли на C#


    Язык С++ известен своими хаками, обходными путями и прочими костылями, чего стоит серия функций XXX_cast. Считается, что в C# такого нет. На самом деле и это не совсем правда…

    Начнем, пожалуй, с нескольких слов:
    • __makeref
    • __reftype
    • __refvalue

    Этих слов нет ни в IntelliSense, да и в MSDN нет официального описания к ним.

    Так что это за чудо-слова такие?

    __makeref принимает объект и возвращает некую «ссылку» на объект в виде объекта типа TypedReference. А, собственно, слова __reftype и __refvalue позволяют из этой «ссылки» узнать соответственно тип объекта и значение объекта по данной «ссылке».

    Рассмотрим пример:
    struct A { public int Index { get; set; } }
    static void Main(string[] args)
    {
      A a = new A();
      a.Index = 10;
      TypedReference reference = __makeref(a);
      Type typeRef = __reftype(reference);
      Console.WriteLine(typeRef); //=> ConsoleApplication23.Program+A
      A valueRef = __refvalue(reference, A);
      Console.WriteLine(valueRef.Index); //=> 10
    }

    Но такой «финт ушами» можно сделать немного более известными средствами:
    static void Main(string[] args)
    {
      A a = new A();
      a.Index = 10;
      dynamic dynam = a;
      Console.WriteLine(dynam.GetType());
      A valuDynam = (A)dynam;
      Console.WriteLine(valuDynam.Index);
    }  

    С dynamic и строк меньше, да и вопросов меньше должно вызывать у людей — «Что это?» и «Как это работает?». Но вот вам немного иной сценарий, где работа с dynamic смотрится не так хорошо, как с TypedReference.
    static void Main(string[] args)
    {
      TypedReference reference = __makeref(a);
      SetVal(reference);
      Console.WriteLine(__refvalue(reference, A).Index);
    }
    static void SetVal(TypedReference reference)
    {
      __refvalue(reference, A) = new A() { Index = 20 };
    }

    Результатом работы будет вывод на консоль числа «20». Да, можно и dynamic через ref в функцию передать и работать будет также.
    static void Main(string[] args)
    {
      dynamic dynam = a;
      SetVal(ref dynam);
      Console.WriteLine(((A)dynam).Index);
    }
    static void SetVal(ref dynamic dynam)
    {
      dynam = new A() { Index = 20 };
    }

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

    Кроме выше описанных, есть еще одно чудо-слово __arglist, которое позволяет сделать функцию с переменным числом параметров, да еще и любого типа.
    static void Main(string[] args)
    {
      Foo(__arglist(1, 2.0, "3", new A[0]));
    }
    public static void Foo(__arglist)
    {
      ArgIterator iterator = new ArgIterator(__arglist);
      while (iterator.GetRemainingCount() > 0)
      {
        TypedReference typedReference = 
          iterator.GetNextArg();
        Console.WriteLine("{0} / {1}",
          TypedReference.ToObject(typedReference),
          TypedReference.GetTargetType(typedReference));
       }
    }

    Странным является то, что нельзя из коробки организовать проход по элементам с помощью foreach, да и напрямую к элементу из списка не обратиться. Так что до С++ или JavaScript c его arguments не дотягивает.:)
    function sum() {
      ....
      for(var i=0; i < arguments.length; i++) 
        s += arguments[i]
    }

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

    Заключение


    В заключение хочется сказать, что и С++ и C# — весьма свободные по грамматике языки, и тем самым с одной стороны удобны в использовании, но с другой не защищают от опечаток. Есть укоренившееся мнение, что в С# нельзя ошибаться так, как в С++, — на самом деле это вовсе не так. В данной статье приведены весьма интересные, на мой взгляд, возможности языка, но львиная доля ошибок в C# состоит не в них, а при написании обычных индукций if, как, например, в проекте Infragistics.
    public bool IsValid
    {
    get {
      var valid = 
        double.IsNaN(Latitude) || double.IsNaN(Latitude) ||
        this.Weather.DateTime == Weather.DateTimeInitial;
      return valid;
     }
    }

    V3001 There are identical sub-expressions 'double.IsNaN(Latitude)' to the left and to the right of the '||' operator. WeatherStation.cs 25

    Внимание рассеивается чаще всего именно в таких моментах, а потом долгие поиски «непонятно чего непонятно где». Так что не упускайте возможность уберечь себя от ошибок с помощью анализатора кода PVS-Studio.

    PVS-Studio
    457.13
    Static Code Analysis for C, C++, C# and Java
    Share post

    Comments 37

      0
      Спрячьте пожалуйста статью под кат.
        +1
        Да, извиняюсь, поправил.
        +3
        Спасибо за статью. Некоторых описанных вещей не знал.
          0
          Всегда пожалуйста, мы работаем для вас.)
          0
          Я думаю что использование @ лучше всего сразу запретить через Coding conventions, так сказать «до особого распоряжения» :)
            +2
            А как же для строк, виндовые пути проще писать.
              +1
              нет — ну конечно же я строки не имел в виду
                0
                а разве там "/" не подходит вместо "\\"?
                  0

                  При копи-пасте длинного пути из проводника, проще поставить @ чем заменять все обратные слэши на прямые вручную.

                    +1
                    Это правда, но можно же не ручками, а заменой в IDE :)
                      0
                      можно конечно — но по времени и удобству все-же @ лучше, да и читаемости "\\" не прибавляют к сожалению
                        +1
                        А вот в регулярках @ действительно незаменима :)
              +2
              Результатом работы, как всегда в этой статье, будет вывод числа «20», а никак не «10».


              и результатом работы следующий программы будет 5, а не 10.

              :)
                –1
                Главное, что не 10:D
              • UFO just landed and posted this here
                  +3
                  Альтернативы? Java не предлагать.
                  • UFO just landed and posted this here
                  +1
                  Статья полезная, приятно, что не пришлось ничего гуглить, а только освежить знания.
                  P.S. Хм, в последнем примере IsValid по семантике больше похоже на IsInvalid.
                    +1
                    > Кроме выше описанных, есть еще одно чудо-слово __arglist, которое позволяет сделать функцию с переменным числом параметров, да еще и любого типа.
                    > Странным является то, что нельзя из коробки организовать проход по элементам с помощью foreach, да и напрямую к элементу из списка не обратиться. Так что до С++ или JavaScript c его arguments не дотягивает.:)

                    Вообще-то и в этом плане дотягивает и ещё мощнее даже — используйте params ( url=https://msdn.microsoft.com/en-us/library/w5zay9db.aspx ). Cтрогая типизация и можно совмещать в одной сигнатуре метода постоянное и переменное число аргументов. Например, такой метод:
                    void Test(int a, params int[] extraArgs) { }
                    // пример вызова
                    Test(1, «extraArg», «extraArg2», «extraArgN»);
                      0
                      params — всё же типизирован, в отличии __arglist. Хотя никто не мешает сделать всё тот же пресловутый dynamic.
                      static void Main(string[] args)
                      {
                        Foo(10, "asd");
                      }
                      public static void Foo(params dynamic[] list)
                      {
                        foreach (var item in list)
                          Console.WriteLine(item);
                      }
                      
                        +4

                        Вот в чем смысл всюду dynamic совать? Конкретно в этом примере достаточно object.

                          +5
                          Немаловажное отличие __arglist от params — в него можно передавать unsafe указатели, а затем доставать их через __refvalue. Указатель нельзя скастовать ни к object ни к dynamic.
                            0
                            А, вот где олигарх зарыт… Век живи — век учись.) Спасибо большое.
                              +4
                              Ну на самом деле пользы от этого никакой, т.к. указатель можно скастовать к IntPtr и таки передать через params.
                              На практике вроде бы единственное реальное применение __arglist — это p/invoke вызовы всяких там printf.
                        0
                        Я не придираюсь, но по поводу пары вещей:

                        А еще можно написать так:

                        class A
                        {
                        public List Numbers => new List();
                        }

                        И в последнем случае вас будет ждать неприятный сюрприз. На самом деле, в последнем примере вы создали вот такое вот свойство:

                        class A
                        {
                        public List Numbers { get { return new List(); } }
                        }


                        А чего еще можно ожидать создав метод который возвращает новый список? Здесь нет никакого сюрприза.

                        Аналогично к
                        Результатом работы будет вывод числа «20», а никак не «10».
                        , вы же сами написали что это мутатор.
                        Про символ @ в документации к языку все сказано, введен чтобы можно было использовать ключевые слова, также ожидаемое поведение.

                        Т.е. про большую часть статьи сказать WTF C# нельзя, поведение соответствует ожидаемому и документации.
                          +1
                          По поводу @ и value.
                          VS подсвечивает, value в set-ре как ключевое слово, и тем самым создавая определенную неоднозначность в этом моменте.
                          В приведенном примере, мы, казалось бы, изменяем одно и тоже поле класса А:
                            get { return @value; }
                            set { @value = value; }
                          

                          Но это не так. Данное поведение происходит из-за не соответствия @value в get и @value в set. @value в get будет являться ничем иным, кроме как, полем класса A. А @value в set на самом деле — это параметр функции set. Таким образом мы просто пишем value само в себя и никак не затрагиваем поле value в классе А.
                          Именно эту суть я и хотел донести. А кстати, в VS2012 и ниже подсветка переменных осуществляется некорректно.

                          А, касаемо
                            public List Numbers => new List();
                          

                          В предыдущем стандарте языка такого не было [Ну или как минимум VS 2013 выдает ошибку синтаксиса]. Вот и хотелось поведать о таких вот способах и предостеречь от ошибок.:)
                            0
                            Действительно, не знал про эту особенность о понимании одного написания по разному в get и set. Хотя идея писать @ вместо this.value вряд ли кому придет.
                          +1
                          На самом деле при помощи __makeref() и __refvalue() реализуется всякий брутальный unsafe в стиле «а теперь берем этот указатель на массив байтов и ловким движением руки превращаем его в указатель на структуру».

                          Я только что побенчил — функция «GenericPtrToStructure» получается примерно в 100 раз быстрее чем лучший ответ на StackOverflow: stackoverflow.com/questions/2871/reading-a-c-c-data-structure-in-c-sharp-from-a-byte-array
                            +3
                            Люто бешенно плюсую за статью! Некоторые моменты открыл для себя.
                            А по поводу замыканий и примера:
                            for (int i = 0; i < numbers.Length; i++)
                                result[i] = numbers.Where(index => index < i);
                            

                            есть добавить пять копеек…

                            Действительно, цикл for захватывает замыкания и значение будет определятся на момент использования результата. «Фиксится» копированиев во временную переменную
                            var localIndex = i;
                            

                            Но вот тут:
                            foreach(var f in fooList)
                            

                            доовольно интересная тема — до C# 5.0 была аналогичная ситуация, что и для for, а вот с выходом указанной версии на каждой итерации создается новая копия f (что-то типа «фикса»).

                            MS официально признала это изменение в языке версии 5.0 ломающим написанный ранее код, но в то же время были вынуждены пойти на этот шаг, так как по концепции foreach скоуп создаваемых переменных внутри foreach.
                              +1
                              Спасибо за статью — как минимум полезно знать подобные «подводные камни» в языке. Особенно редко встречающиеся в обычной жизни.
                                +1
                                Многие упомянутые вещи с натяжкой можно назвать «подводными камнями». Больше похоже на намеренный выстрел себе в ногу.
                                  0
                                  согласен — но о таких возможностях тоже нелишне помнить и знать
                                  +1
                                  Хорошая статья — дает представление о возможных пунктах, которые стоит упомянуть в coding conditions…
                                  • UFO just landed and posted this here
                                      0
                                      Да.
                                      Взять в кавычки — это наиболее простой вариант описания подобного синтаксиса.
                                      0
                                      Хехе… public void Foo(__arglist) напрочь вышибает VS2015. Может в resharper проблема, но студия крашится через 2 секунды :).
                                        0
                                        Данной проблемы я у себя не наблюдал. Но я фанатик максимально чистой студии без дополнительных плагинов, за исключением PVS-Studio.:)

                                      Only users with full accounts can post comments. Log in, please.