Для получения триального ключа
заполните форму ниже
Team License (базовая версия)
Enterprise License (расширенная версия)
* Нажимая на кнопку, вы даете согласие на обработку
своих персональных данных. См. Политику конфиденциальности

** На сайте установлена reCAPTCHA и применяются
Политика конфиденциальности и Условия использования Google.
Запросите информацию о ценах
Новая лицензия
Продление лицензии
--Выберите валюту--
USD
EUR
GBP
RUB
* Нажимая на кнопку, вы даете согласие на обработку
своих персональных данных. См. Политику конфиденциальности

** На сайте установлена reCAPTCHA и применяются
Политика конфиденциальности и Условия использования Google.
Для получения лицензии для вашего открытого
проекта заполните, пожалуйста, эту форму
* Нажимая на кнопку, вы даете согласие на обработку
своих персональных данных. См. Политику конфиденциальности

** На сайте установлена reCAPTCHA и применяются
Политика конфиденциальности и Условия использования Google.
Для получения лицензии для вашего открытого
проекта заполните, пожалуйста, эту форму
* Нажимая на кнопку, вы даете согласие на обработку
своих персональных данных. См. Политику конфиденциальности

** На сайте установлена reCAPTCHA и применяются
Политика конфиденциальности и Условия использования Google.
Мне интересно попробовать плагин на:
* Нажимая на кнопку, вы даете согласие на обработку
своих персональных данных. См. Политику конфиденциальности

** На сайте установлена reCAPTCHA и применяются
Политика конфиденциальности и Условия использования Google.
Ваше сообщение отправлено.

Мы ответим вам на


Если вы так и не получили ответ, пожалуйста, проверьте папку
Spam/Junk и нажмите на письме кнопку "Не спам".
Так Вы не пропустите ответы от нашей команды.

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

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

15 Июн 2016

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

0403_Amusing_CSharp_ru/image1.png

Ремарка.

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

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

Мы все знаем, что свойства - это пара функций: аксессор и мутатор, для изменения и чтения значения в каком-то поле. Ну, или по крайней мере так было до версии языка 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 с его 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 оказался внимательнее, чем три с половиной программиста

Дата: 22 Окт 2018

Автор: Андрей Карпов

PVS-Studio, как и другие статические анализаторы кода, часто выдаёт ложные срабатывания. Но не стоит спешить считать странные срабатывания ложными. Это короткая история о том, как PVS-Studio вновь ок…
Характеристики анализатора PVS-Studio на примере EFL Core Libraries, 10-15% ложных срабатываний

Дата: 31 Июл 2017

Автор: Андрей Карпов

После большой статьи про проверку операционной системы Tizen мне было задано много вопросов о проценте ложных срабатываний и о плотности ошибок (сколько ошибок PVS-Studio выявляет на 1000 строк кода)…
Эффект последней строки

Дата: 31 Май 2014

Автор: Андрей Карпов

Я изучил множество ошибок, возникающих в результате копирования кода. И утверждаю, что чаще всего ошибки допускают в последнем фрагменте однотипного кода. Ранее я не встречал в книгах описания этого …
PVS-Studio для Java

Дата: 17 Янв 2019

Автор: Андрей Карпов

В седьмой версии статического анализатора PVS-Studio мы добавили поддержку языка Java. Пришло время немного рассказать, как мы начинали делать поддержку языка Java, что у нас получилось и какие дальн…
Бесплатный PVS-Studio для тех, кто развивает открытые проекты

Дата: 22 Дек 2018

Автор: Андрей Карпов

В канун празднования нового 2019 года команда PVS-Studio решила сделать приятный подарок всем контрибьюторам open-source проектов, хостящихся на GitHub, GitLab или Bitbucket. Им предоставляется возмо…
Как и почему статические анализаторы борются с ложными срабатываниями

Дата: 20 Мар 2017

Автор: Андрей Карпов

В своей предыдущей статье я писал, что мне не нравится подход, при котором статические анализаторы кода оцениваются с помощью синтетических тестов. В статье приводился пример, воспринимаемый анализат…
Любите статический анализ кода!

Дата: 16 Окт 2017

Автор: Андрей Карпов

Я в шоке от возможностей статического анализа кода, хотя сам участвую в разработке инструмента PVS-Studio. На днях я был искренне удивлён тому, что анализатор оказался умнее и внимательнее меня.
Технологии, используемые в анализаторе кода PVS-Studio для поиска ошибок и потенциальных уязвимостей

Дата: 21 Ноя 2018

Автор: Андрей Карпов

Краткое описание технологий, используемых в инструменте PVS-Studio, которые позволяют эффективно обнаруживать большое количество паттернов ошибок и потенциальных уязвимостей. Статья описывает реализа…
Статический анализ как часть процесса разработки Unreal Engine

Дата: 27 Июн 2017

Автор: Андрей Карпов

Проект Unreal Engine развивается - добавляется новый код и изменятся уже написанный. Неизбежное следствие развития проекта - появление в коде новых ошибок, которые желательно выявлять как можно раньш…
Главный вопрос программирования, рефакторинга и всего такого

Дата: 14 Апр 2016

Автор: Андрей Карпов

Вы угадали, ответ - "42". Здесь приводится 42 рекомендации по программированию, которые помогут избежать множества ошибок, сэкономить время и нервы. Автором рекомендаций выступает Андрей Карпов - тех…

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

Следующие комментарии

На сайте установлена reCAPTCHA и применяются
Политика конфиденциальности и Условия использования Google.
Этот сайт использует куки и другие технологии, чтобы предоставить вам более персонализированный опыт. Продолжая просмотр страниц нашего веб-сайта, вы принимаете условия использования этих файлов. Если вы не хотите, чтобы ваши данные обрабатывались, пожалуйста, покиньте данный сайт. Подробнее →
Принять