Не секрет, что C# сегодня популярный и динамично развывающийся язык, в отличие от своего прямого конкурента — языка Java, который в плане функциональности переживает период застоя. Основное неоспоримое преимущество Java — настоящая кросплатформенность, а не унылая и ограниченная, как у C#.
C# — простой язык, благодаря простоте живёт и PHP. Но в то же время он весьма функциональный, и имеет статус «гибридного» языка, совмещая в себе различные парадигмы, встроенную поддержку как императивного стиля программирования, так и функционального.
Как и любой язык, шарп имеет свои тонкости, особенности, «подводные камни» и малоизвестные возможности. Что я имею ввиду? Читайте под катом…
Ссылочные типы (object, dynamic, string, class, interface, delegate) хранятся в управляемой куче, типы значений (struct, enum; bool, byte, char, int, float, double) — в стеке приложения (кроме случая, когда тип значения является полем класса). Преобразование типа значений к ссылочному типу сопровождается неявной операцией упаковки (boxing) — помещение копии типа значений в класс-обёртку, экземпляр которого сохраняется в куче. Упаковочный тип генерируется CLR и реализует интерфейсы сохраняемого типа значения. Преобразование ссылочного типа к типу значений вызывает операцию распаковки (unboxing) — извлечение из упаковки копии типа значения и помещение её в стек.
Соответствующий IL-код:
Упаковка и распаковка являются относительно медленными операциями (подразумевают копирование), поэтому по возможности следует их избегать. Нижеследующий код отображает неочевидные случаи, приводящие к упаковке:
В msdn рекомендуется избегать типов значений в случаях, когда они должны быть упакованы много раз, например в не универсальных классах коллекций (ArrayList). Упаковки-преобразования типов значений можно избежать с помощью универсальных коллекций (System.Collections.Generic namespace). Также следует помнить, что dynamic на уровне IL-кода — это тот же object, только (не всегда) помеченный атрибутами.
Обратимся к классической реализации рекурсивного вычисления факториала при помощи лямбда-выражений:
Мы знаем, что лямбда способна ссылаться на саму себя благодаря способности захватывать переменные окружения (замыкание). Мы знаем, что захваченные объекты могут быть изменены вне контекста лямбда-выражения, и не существует встроенной в язык возможности переопределить такое поведение. Понятно, что вбольшинстве ряде случаев такой подход не будет приемлемым, и хотелось бы как-то ограничить возможность изменять захваченную переменную вне контекста лямбды.
В общем случае представленная проблема решается реализацией комбинатора неподвижной точки:
При помощи механизма отражения можно изменить значение даже private-поля класса.
Понятно, что применять это строить только в случае крайней необходимости, соблюдая принцип инкапсуляции.
UPD: Как справедливо заметил braindamaged, изменить приватное поле удастся только если сборка принадлежит группе кода, располагающей необходимыми полномочиями. Затребовать такое полномочие можно декларативно, пометив класс (метод) чем-то вроде этого:
С системой безопасности .NET не всё просто, причём в .NET 4 она претерпела серьёзные изменения.
Чтобы иметь возможность итерировать по элементам экземпляра некоторого класса при помощи foreach, достаточно реализовать в нём метод GetEnumerator().
Это небольшое проявление так называемой «утиной» типизации, обычно применяемой в динамических языках, имеет место и в C#.
Переменные анонимного типа можно сохранять в коллекции. Убедитесь сами:
Переменные анонимного типа можно передавать в другую область видимости:
Начиная с версии C# 4.0 ключевое слово ref можно опускать при вызове метода через COM Interop. В сочетании с именованными аргументами выглядит весьма эффектно:
Заметьте: именованные параметры и возможность опускать ref — это средства языка, поэтому в качестве базового фреймворка приложения может быть выбран как .NET Framework 4.0, так и .NET Framework 2.0, 3.0, 3.5.
Среди всех прочих «тонкостей» языка я бы выделил проблему детерминированного уничтожения объектов, сложность обработки асинхронных исключений типа ThreadAbortException. Интерес представляют мощные средства синхронизации потоков и грядущие изменения в C# 5.0, связанные со встраиванием в язык поддержки асинхронных операций.
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, связанные со встраиванием в язык поддержки асинхронных операций.