8 фактов, которые вы, возможно, не знали о C#

в 20:33, , рубрики: .net, особенности, факты

Вот несколько необычных фактов о языке C#, о которых знают лишь немногие разработчики.

1. Индексаторы могут использовать params параметры

Мы все знаем, как обычно выглядят индексаторы x = something["a"], а так же код необходимый для его реализации:

public string this[string key]
 {
   get { return internalDictionary[key]; }
 }

Но знали ли вы, что для доступа к элементам вы можете использовать params параметры x = something["a", "b", "c", "d"]?
Просто напишите ваш индексатор следующим образом:

public IEnumerable<string> this[params string[] keys]
 {
   get { return keys.Select(key => internalDictionary[key]).AsEnumerable(); }
 }

Отличная новость заключается в том, что вы можете использовать несколько индексаторов совместно в одном классе. Если кто-то передаст массив или несколько аргументов в качестве параметра он получит IEnumerable как результат, но вызывая, индексатор с одним параметром он получит единственное значение.

2. Строковые литералы, определенные в вашем коде хранятся в единственном экземпляре

Многие разработчики думают, что следующий код:

if (x == "" || x == "y")

будет создавать пару строк каждый раз. Нет, не будет. C# как и многие другие языки программирования использует интернирование строк, и каждая литеральная строка используемая в вашем приложении кладется в специальный список (хеш таблицу), называемый пулом строк, откуда и берется ссылка на нее во время выполнения.

Вы можете использовать метод String.IsInterned, чтобы определить находится ли строка в пуле интернирования в настоящий момент, но помните, что выражение String.IsInterned("what") == "what" всегда будет истинно.
String.IsInterned("wh" + "at") == "what" так же будет всегда истинно, спасибо компилятору за оптимизацию. String.IsInterned(new string(new char[] {'w','h','a','t'}) == new string(new char[] {'w','h','a','t'}) будет истинно только, если литеральная строка «what» уже была использована в вашем коде или если она вручную была добавлена в пул интернирования.

Если у вас есть классы, которые регулярно используют строки, рассмотрите возможность использования метода String.Intern для добавления их в пул строк. Однако будьте осторожны, так как в этом случае они хранятся до завершения приложения, поэтому используйте String.Intern аккуратно. Для добавления строки в пул просто вызовите метод String.Intern(someClass.ToString()). Другая предосторожность заключается в том, что (object)"Hi" == (object)"Hi" всегда истинно, спасибо интернированию. Попробуйте проверить этот код в окне интерпретации, и результат будет отрицательным, так как отладчик не интернирует ваши строки.

3. Приведение типов к менее специализированным не мешает использовать их настоящий тип

Прекрасным примером этого является свойство возвращающее IEnumerable, настоящий тип которого есть List, например:

private readonly List<string> internalStrings = new List<string>();
public IEnumerable<string> AllStrings { get { return internalStrings; }

Вы думаете, что никто не может изменить список строк. Увы, это сделать слишком просто:

((List<string>)x.AllStrings).Add("Hello");

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

4. Переменные в методах могут иметь область видимости ограниченную фигурными скобками

В языке программирования Pascal вы должны описать все переменные, которые будут использованы в начале вашей функции. К счастью, сегодня объявления переменных могут существовать рядом с их присваиванием, что предотвращает их случайное использование, прежде чем вы намеревались.

Однако это не останавливает вас, их использовать после того, как они перестали быть нужными. Мы можем описать временные переменные, используя ключевые слова for/if/while/using. Вы будете удивлены, но вы так же можете описать временные переменные, используя фигурные скобки без ключевых слов для получения того же результата:

private void MultipleScopes()
 {
   { var a = 1; Console.WriteLine(a); }
   { var b = 2; Console.WriteLine(a); }
 }

Данный код не компилируется из-за второй строчки, потому что она обращается к переменной, которая заключена в фигурные скобки. Такое разбиение метода на части фигурными скобками достаточно полезно, но гораздо лучшее решение — разделить метод, на меньшие, используя метод рефакторинга: выделение метода (extract method).

5. Перечисления могут иметь методы расширения

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

enum Duration { Day, Week, Month };
static class DurationExtensions
 {
   public static DateTime From(this Duration duration, DateTime dateTime)
    {
     switch (duration)
      {
         case Duration.Day: return dateTime.AddDays(1);
         case Duration.Week: return dateTime.AddDays(7);
         case Duration.Month: return dateTime.AddMonths(1);
         default: throw new ArgumentOutOfRangeException("duration");
        }
    }
 }

Я думаю перечисления — зло, но, по крайней мере, методы расширения позволяют избавиться от некоторых условных проверок (switch/if) в вашем коде. Не забудьте проверить, что значение перечисления находится в нужном диапазоне.

6. Порядок, в котором вы описываете статические переменные, имеет значение

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

static class Program
 {
   private static int a = 5;
   private static int b = a;

   static void Main(string[] args)
    {
       Console.WriteLine(b);
    }
 }

Этот код будет печатать число 5. Если поменять местами описание переменных a и b, он будет печатать 0.

7. Private переменная экземпляра класса может быть доступна в другом экземпляре данного класса

Вы можете подумать, что следующий код работать не будет:

class KeepSecret
 {
   private int someSecret;
   public bool Equals(KeepSecret other)
    {
      return other.someSecret == someSecret;
    }
  }

Легко подумать, что private означает только данный экземпляр класса, может иметь доступ к нему, но в реальности это означает, что только данный класс имеет доступ к нему… включая другие экземпляры данного класса. На самом деле это очень полезно при реализации некоторых методов сравнения.

8.Спецификация языка C# уже на вашем компьютере

Если у вас установлена Visual Studio вы можете найти спецификацию в папке VC#Specifications. VS 2011 поставляется вместе со спецификацией языка C# 5.0 в формате Word.

Она полна множества интересных фактов таких как:

  • i = 1 — атомарно (потокобезопастно) для int но не для long;
  • Вы можете использовать операторы & и |к обнуляемому логическому типу (bool?), обеспечивая совместимость с троичной логикой SQL;
  • Использование [Conditional(«DEBUG»)] предпочтительнее, чем #if DEBUG.

А тем из вас кто говорит, «я знал все/большинство из этого» я говорю: «Где вы, когда я нанимаю разработчиков?». Серьезно, достаточно трудно найти C# разработчика с хорошим знанием языка.

От себя добавлю пару фактов:

9. Вы не можете использовать константу с именем value__ в перечислении

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

Следующее перечисление:

public enum Language
 {
   C,
   Pascal,
   VisualBasic,
   Haskell
 }

компилятор видит примерно так:

public struct Language : Enum
 {
   //Открытые константы, определяющие символьные имена и значения
   public const Language C = (Language)0;
   public const Language Pascal = (Language)1;
   public const Language VisualBasic = (Language)2;
   public const Language Haskell = (Language)3;

   //Открытое поле экземпляра со значением переменной Language
   //Код с прямой ссылкой на этот экземпляр невозможен
    public Int32 value__;
  }

Теперь понятно, что имя переменной value__ просто зарезервировано для хранения текущего значения перечисления.

10. Инициализатор полей позволяет слишком много

Все мы знаем, как работает инициализатор полей.

class ClassA
 {
   public string name = "Tim";
   public int age = 20;

   public ClassA()
    { }

   public ClassA(string name, int age)
    {
     this.name = name;
     this.age = age;
    }
  }

Код инициализации просто помещается в каждый из конструкторов.

class ClassA
  {
    public string name;
    public int age;

    public ClassA()
    {
      this.name = "Tim";
      this.age = 20;
      base..ctor();
    }

    public ClassA(string name, int age)
    {
      this.name = "Tim";
      this.age = 20;
      base..ctor();
      this.name = name;
      this.age = age;
    }
  }

Но если один конструктор вызывает другой, то инициализация помещается только в вызываемый конструктор…

class ClassA
 {
   public string name = "Tim";
   public int age = 20;

   public ClassA()
    { }

   public ClassA(string name, int age):this()
    {
     this.name = name;
     this.age = age;
    }
 }

что равносильно

class ClassA
  {
    public string name;
    public int age;

    public ClassA()
    {
      this.name = "Tim";
      this.age = 20;
      base..ctor();
    }

    public ClassA(string name, int age)
    {
      this..ctor();
      this.name = name;
      this.age = age;
    }
  }

А теперь вопрос, что будет, если конструкторы вызывают друг друга? Куда компилятор решит поместить код инициализации?

Ответ: компилятор решит, что раз первый конструктор вызывает второй, то в первый инициализацию не помещу. Аналогичные выводы он сделает со вторым конструктором. В результате код инициализации не будет помещен ни в один конструктор… А раз его не надо никуда помещать, то зачем тогда его проверять на наличие ошибок? Таким образом в коде инициализации можно писать все, что угодно. Например,

class ClassA
 {
   public string name = 123456789;
   public int age = string.Empty;

   public ClassA():this("Alan", 25)
    { }

   public ClassA(string name, int age):this()
    {
     this.name = name;
     this.age = age;
    }
 }

Такой код скомпилируется без проблем, однако грохнется с переполнением стека при создании экземпляра данного класса.

11. Вы можете использовать только C# синонимы при описании базового типа перечисления

Как известно при описании перечисления мы можем явно указать базовый тип, лежащий в его основе. По умолчанию — это int.

Мы можем написать так

public enum Language : int
 {
   C,
   Pascal,
   VisualBasic,
   Haskell
 }

однако мы не можем написать так

public enum Language : Int32
 {
   C,
   Pascal,
   VisualBasic,
   Haskell
 }

Компилятор требует именно C# синоним типа.

12. Структуры являются неизменяемыми, когда используются в коллекциях

Как известно структуры в .NET являются значимыми типами, то есть они передаются по значению (копируются), именно поэтому изменяемые структуры — зло. Однако тот факт, что изменяемые структуры — зло еще не говорит о том, что их нельзя изменять. По умолчанию структуры являются изменяемыми, то есть их состояние можно изменить. Но если структуры используются в коллекциях, то они перестают быть изменяемыми. Рассмотрим код:

struct Point
 {
   public int x;
   public int y;
 }

static void Main(string[] args)
 {
    var p = new Point();  p.x = 5; p.y = 9;
    var list = new List<Point>(10);
    list.Add(p);
    list[0].x = 90;//ошибка компиляции

    var array = new Point[10];
    array[0] = p;
    array[0].x = 90;//все ок
}

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

Спасибо за прочтение.
Надеюсь, статья оказалась полезной.

Автор: timyrik20

Источник

Поделиться

* - обязательные к заполнению поля