Тонкие моменты C#

C SharpНе секрет, что C# сегодня популярный и динамично развывающийся язык, в отличие от своего прямого конкурента — языка Java, который в плане функциональности переживает период застоя. Основное неоспоримое преимущество Java — настоящая кросплатформенность, а не унылая и ограниченная, как у C#.

C# — простой язык, благодаря простоте живёт и PHP. Но в то же время он весьма функциональный, и имеет статус «гибридного» языка, совмещая в себе различные парадигмы, встроенную поддержку как императивного стиля программирования, так и функционального.

Как и любой язык, шарп имеет свои тонкости, особенности, «подводные камни» и малоизвестные возможности. Что я имею ввиду? Читайте под катом…

Упаковка и распаковка — знают все, да не каждый


Ссылочные типы (object, dynamic, string, class, interface, delegate) хранятся в управляемой куче, типы значений (struct, enum; bool, byte, char, int, float, double) — в стеке приложения (кроме случая, когда тип значения является полем класса). Преобразование типа значений к ссылочному типу сопровождается неявной операцией упаковки (boxing) — помещение копии типа значений в класс-обёртку, экземпляр которого сохраняется в куче. Упаковочный тип генерируется CLR и реализует интерфейсы сохраняемого типа значения. Преобразование ссылочного типа к типу значений вызывает операцию распаковки (unboxing) — извлечение из упаковки копии типа значения и помещение её в стек.
using System;

class Program
{
  static void Main()
  {
    int val = 5;
    object obj = val;     // присваивание сопровождается упаковкой
    int valUnboxed = (int)obj; // приведение вызовет распаковку
  }
}


Соответствующий IL-код:

.locals init ([0] int32 val, [1] object obj, [2] int32 valUnboxed)
IL_0000: nop
IL_0001: ldc.i4.5
IL_0002: stloc.0
IL_0003: ldloc.0
IL_0004: box [mscorlib]System.Int32
IL_0009: stloc.1
IL_000a: ldloc.1
IL_000b: unbox.any [mscorlib]System.Int32
IL_0010: stloc.2
IL_0011: ret


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

class Program
{
  static void Main()
  {
    // 1. Преобразование типа значений в ссылку на реализуемый им интерфейс
    IComparable<int> iComp = 1;
    // 2. Преобразование типа enum в ссылку на System.Enum
    Enum format = UriFormat.Unescaped;
    // 3. Преобразование типа значений к типу dynamic
    dynamic d = 1;
  }
}


В msdn рекомендуется избегать типов значений в случаях, когда они должны быть упакованы много раз, например в не универсальных классах коллекций (ArrayList). Упаковки-преобразования типов значений можно избежать с помощью универсальных коллекций (System.Collections.Generic namespace). Также следует помнить, что dynamic на уровне IL-кода — это тот же object, только (не всегда) помеченный атрибутами.

Рекурсия в лямбдах — о зловредном замыкании


Обратимся к классической реализации рекурсивного вычисления факториала при помощи лямбда-выражений:
using System;
using System.Numerics;

class Program
{
  static void Main()
  {
    Func<int, BigInteger> fact = null;
    fact = x => x > 1 ? x * fact(x - 1) : 1;
  }
}


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

В общем случае представленная проблема решается реализацией комбинатора неподвижной точки:
using System;
using System.Numerics;

class Program
{
  static void Main()
  {
    var fact = YPointCombinator.Create<int, BigInteger>(f => (n) => n > 1 ? n * f(n - 1) : 1);
    var power = YPointCombinator.Create<int, int, BigInteger>(f => (x, y) => y > 0 ? x * f(x, y - 1) : 1);
  }
}
public static class YPointCombinator
{
  public static Func<T1, T2> Create<T1, T2>(Func<Func<T1, T2>, Func<T1, T2>> f)
  {
    return f(r => Create( f )( r ));
  }
  public static Func<T1, T2, T3> Create<T1, T2, T3>(Func<Func<T1, T2, T3>, Func<T1, T2, T3>> f)
  {
    return f((r1, r2) => Create(f)(r1, r2));
  }
}


Поля private и рефлексия, или плевали мы на ваше ООП


При помощи механизма отражения можно изменить значение даже private-поля класса.
Понятно, что применять это строить только в случае крайней необходимости, соблюдая принцип инкапсуляции.
using System;
using System.Reflection;

class Sample
{
  private string _x = "No change me!";
  public override string ToString()
  {
    return _x;
  }
}
class Program
{
  static void Main()
  {
    var sample = new Sample();
    typeof(Sample).GetField("_x", BindingFlags.NonPublic | BindingFlags.Instance)
           .SetValue(sample, "I change you...");
    Console.Write(sample);
    Console.ReadKey();
  }
}


UPD: Как справедливо заметил braindamaged, изменить приватное поле удастся только если сборка принадлежит группе кода, располагающей необходимыми полномочиями. Затребовать такое полномочие можно декларативно, пометив класс (метод) чем-то вроде этого:
[System.Security.Permissions.ReflectionPermission(System.Security.Permissions.SecurityAction.Assert)]

С системой безопасности .NET не всё просто, причём в .NET 4 она претерпела серьёзные изменения.

«Утиная» типизация и цикл foreach


Чтобы иметь возможность итерировать по элементам экземпляра некоторого класса при помощи foreach, достаточно реализовать в нём метод GetEnumerator().
using System;
using System.Collections.Generic;

class Sample
{
  public IEnumerator<int> GetEnumerator()
  {
    for (var i = 0; i < 10; ++i)
      yield return i;
  }
}
class Program
{
  static void Main()
  {
    foreach (var t in new Sample())
      Console.WriteLine(t);
    Console.ReadKey();
  }
}


Это небольшое проявление так называемой «утиной» типизации, обычно применяемой в динамических языках, имеет место и в C#.

Анонимные типы — можно больше


Переменные анонимного типа можно сохранять в коллекции. Убедитесь сами:
using System;
using System.Linq;

class Program
{
  static void Main()
  {
    var list = new[] {
      new { Name = "Alex", Age = 18 },
      new { Name = "Petr", Age = 30 } }.ToList();

    Console.Write(list.Find(x => x.Name == "Petr"));
    Console.ReadKey();
  }
}


Переменные анонимного типа можно передавать в другую область видимости:
using System;

class Program
{
  static dynamic User
  {
    get { return new { Name = "Alex", Age = 18 }; }
  }

  static void Main()
  {
    Console.Write(User.Name);
    Console.ReadKey();
  }
}


ref иногда можно опустить


Начиная с версии C# 4.0 ключевое слово ref можно опускать при вызове метода через COM Interop. В сочетании с именованными аргументами выглядит весьма эффектно:
using System;
using Word = Microsoft.Office.Interop.Word;

class Program
{
  static void Main()
  {
    var app = new Word.Application();
    Word.Document doc = null;

    // C# 2.0 - 3.5
    object
      filename = "test.doc",
      visible = true,
      missing = Type.Missing;

    doc = app.Documents.Open(
      ref filename, ref missing, ref missing, ref missing, ref missing, ref missing,
      ref missing, ref missing, ref missing, ref missing, ref missing, ref visible,
      ref missing, ref missing, ref missing, ref missing);

    // C# 4.0
    doc = app.Documents.Open(FileName: "test.doc", Visible: true);
  }
}


Заметьте: именованные параметры и возможность опускать ref — это средства языка, поэтому в качестве базового фреймворка приложения может быть выбран как .NET Framework 4.0, так и .NET Framework 2.0, 3.0, 3.5.

Что осталось за кадром


Среди всех прочих «тонкостей» языка я бы выделил проблему детерминированного уничтожения объектов, сложность обработки асинхронных исключений типа ThreadAbortException. Интерес представляют мощные средства синхронизации потоков и грядущие изменения в C# 5.0, связанные со встраиванием в язык поддержки асинхронных операций.
AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 32

    0
    Упаковка действительно «тяжёлая» операция, так как требует выделения памяти. Распаковка — нет, это только копирование. При прямой работе со структурами тоже происходит копирование. Так что при распаковке никаких особых потерь в скорости нет.
      +5
      MSDN с вами не согласен.

      Цит. (смысловой перевод): упаковка и распаковка — дорогие операции. При упаковке типа значения должен быть создан новый объект. Такая операция может занять до 20 раз больше времени, чем присваивание. При распаковке операция приведения может занять в 4 раза больше времени, чем нужно для присваивания (а распаковка, как известно, всегда сопровождается явным приведением).
        +5
        Если произвести самостоятельное тестирование, то окажется, что операции упаковки и распаковки ничтожно влияют на выполнение кода, только если вы не будете «гонять» очень тяжелые значимые типы из кучи в стек и обратно. Подробнее здесь
          0
          Мне тоже кажется, что ребята из Microsoft завышают расценки — в 20 раз, в 4 раза. Однако часть проблемы в том, что операции упаковки-распаковки в некотором смысле «парные» — если есть упаковка, значит где-то понадобится и распаковка (не факт, конечно). Второе — это неявное создание копии объекта, что тоже нужно держать в уме. Всё это сочетание всё же достаточно существенно снижает производительность, чтобы сделать выбор, например, в сторону обобщений вместо object или dynamic.

          Не думаю, что вселенная взорвётся от того, что где-то в коде произойдёт десяток упаковок-распаковок. Но на коллекциях объектов, при массовых «перепаковках» всё же стоит задуматься о производительности.
          +3
          Плюнь на MSDN. Вот код теста.
          pastebin.com/zMReV0Bj

          У меня он выводит:

          Boxing: 00:00:13.9155034
          Unboxing: 00:00:00.9500705
          Copying: 00:00:00.7068114
        +2
        если вы действительно хотите поговорить про «тонкие моменты», то не говорите, что value – stack; reference – heap. посмотрите сначала на то, что пишет Эрик Липперт blogs.msdn.com/b/ericlippert/archive/2010/09/30/the-truth-about-value-types.aspx
        да и остальное на тонкости не сильно тянет.
          +1
          Наверное, название не совсем точно отображает суть. Скорее, «интересные моменты» или «некоторые моменты». Не суть. Первая «проба пера» всё же. Буду совершенствоваться.
          +2
          Хм. Ну раз тут «тонкости», то можно и позанудствовать.

          >> Ссылочные типы (...) хранятся в управляемой куче
          Не типы, а инстансы объектов этих типов. Сами типы хранятся тоже в аналоге кучи, но CLR вас в той куче видеть не хотела бы.

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

          >> [Про рефлексию]… соблюдая принцип инкапсуляции.
          Не забудьте соблюсти permissions, ибо под FullTrust != всегда

          Ни о чем, если честно. Какова объединяющая мысль статьи?
            +1
            1) Да, конечно, имеются ввиду экземпляры объектов ссылочного типа. Другое дело, что опускаю эти «подробности» не один я.

            2) Тут можно немного пофилософствовать на тему того, что массивы в платформе .NET Framework реализуются как экземпляры класса System.Array, но если рассматривать массив как структуру данных, то вы правы. Массив — ссылочный тип, следовательно, память для него распределяется в куче.

            3) А можно поподробнее?

            Объединяющая мысль статьи — в C# есть особенности/возможности, о которых вы можете не знать/догадываться. Да прибудет с вами знание этой статьи!
            0
            кроме того в сильверлайте не все можно сделать из того, что можно в обычном дотнете
              0
              А в Windows Phone даже не всегда можно то, что можно в сильверлайте :(
            +6
            Автор, почитай Рихтера.

            То, что ты описал не является чем-то из ряда вон выходящим.
              +4
              У меня не было цели написать что-то из ряда вон выходящего. Всё мною описанное прямым или косвенным образом следует из спецификации языка, потому что по-другому и быть не может. Ничего не имею против, если кто-то всё это знает, а многие знают и больше. Но многие все учатся, и, надеюсь, я помог прояснить им некоторые моменты, дать небольшие рекомендации, конспективно отображая суть проблемы и её решение.
                +4
                Я ценю ваш труд, но этот топик из того разряда, что те кому надо уже знают об этом.
                Ну раз уж опубликовано, то будем надеятся, что это поможет кому-то.

                Если есть желаение написать что-то особенное, то было бы хорошо увидеть тему про взаимодействие managed/unmanaed (как с той, так и с другой стороны) со всеми тонкостями указателей (IntPtr, void* etc.), маршаллинга(Marshal), UnmanagedMemoryStream, StructLayout и прочие.

                Ну это так, ИМХО.
                  +2
                  Беру на заметку.
              0
              В примере про GetEnumerator, типизация не совсем duck.
              t в цикле main имеет тип Object.
              По моему лучше типизировать:

              using System.Collections.Generic;
                0
                public IEnumerator GetEnumerator()
                  0
                  *случайно отправилось.
                  class Sample
                  {
                  public IEnumerator GetEnumerator()

                  }

                  А за СОМ сахар спасибо, не знал.
                    0
                    а теперь хабр теги сожрал
                    class Sample
                    {
                    public IEnumerator < int > GetEnumerator()
                    ...
                    }
                      0
                      Всё верно, лучше типизировать. Подправил.
                    0
                    В любом случае это не duck.
                    foreach скрывает как раз вызов метода GetEnumerator() и тип объекта не приводится к IEnumerable
                      0
                      угу, foreach — это никак не duck typing. duck typing в C# — это объекты dynamic.
                        +2
                        Оператор foreach требует, чтобы объект итераций реазизовывал метод с определённой сигнатурой (контракт), но не требует от этого объекта реализации к.-л. интерфейса (такого, как IEnumerable). Если прочитать определение с вики, то именно такое поведение и считается утиной типизацией.

                        Процитирую:
                        >> Утиная типизация — вид динамической типизации, применяемой в некоторых языках программирования, когда границы использования объекта определяются его текущим набором методов и свойств, в противоположность наследованию от определённого класса. То есть считается, что объект реализует интерфейс, если он содержит все методы этого интерфейса, независимо от связей в иерархии наследования и принадлежности к какому-либо конкретному классу.

                        В нашем случае можно сказать, что оператор foreach считает объект коллекцией, если тот реализует метод GetEnumerator().
                          0
                          Да, согласен. Ступил, извиняюсь
                    +1
                    >>>При помощи механизма отражения можно изменить значение даже private-поля класса.
                    Понятно, что применять это строить только в случае крайней необходимости, соблюдая принцип инкапсуляции.

                    Более того можно менять readonly поля.
                      +2
                      после предложения «C# — простой язык, благодаря простоте живёт и PHP.» завис.

                      этот тонкий момент точно не все знают:
                      типы значений хранятся в стеке приложения (кроме случая, когда тип значения является полем класса).
                        0
                        Если быть точным, то типы значений храняться в стеке потока.
                          0
                          Если быть точным, то типы значений обычно храняться в стеке потока.
                            0
                            Если быть ещё точнее, то типы значений обычно хранятся в стеке логического управляемого потока .NET, который не обязательно однозначно отображается на физический поток операционной системы.

                            В итоге имеем: объекты типов значений хранятся в стеке управляемого потока среды .NET, за исключением случаев, когда объект типа значения является полем класса или элементом массива. С другой стороны, объекты ссылочного типа всегда размещаются в управляемой куче процесса.

                            p.s. Язык C++/CLI позволяет создать массив значений в стеке.
                        0
                        Вот вот! Фраза «благодаря простоте живет и PHP» просто убила…
                          0
                          Да, не совсем красиво… Имелось ввиду, что свою широкую популярность язык PHP приобрёл в том числе и благодаря своей простоте. Навеяно вот этим.

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