Avatar ·

Скрытые возможности C#, о которых молчат в учебниках

Всем привет! Я пишу на C# около года, прочитал несколько книг (Шилдт, Троелсен), но чувствую, что язык гораздо глубже, чем кажется. На StackOverflow наткнулся на тред "Hidden Features of C#" с 760к просмотров, но там всё на английском. Меня интересуют именно те фишки, которые не очевидны новичку. Например, я недавно узнал про `nameof()` — кажется, мелочь, а помогает при рефакторинге. Или про `??` (null-coalescing) — раньше писал длинные if-ы. Но есть вещи, которые я вообще не понимаю. Например, что такое `yield return`? Видел в примерах, но не могу осознать, зачем это нужно, если можно просто вернуть List. Ещё слышал про "extension methods" — вроде добавляешь методы к существующим типам, но как это работает внутри? И самый большой вопрос: правда ли, что в C# есть возможность писать код, который выполняется до Main()? Или это миф? Поделитесь, пожалуйста, самыми полезными скрытыми фишками C#, которые реально упрощают жизнь. Интересуют как синтаксические сахара (вроде `=>` для свойств), так и серьёзные возможности (например, unsafe-код или деревья выражений). Примеры кода приветствуются! P.S. Если знаете фишки, которые появились в C# 8-12 — тоже пишите, хочется быть в курсе современного синтаксиса. <<>>

Расширяем горизонты: от yield до контекстных ключевых слов

Отличный вопрос! C# действительно полон «пасхальных яиц», которые превращают рутинный код в элегантные решения. Давайте разберём несколько самых мощных фишек, которые стоит знать каждому разработчику.

1. yield return — ленивая магия

Вы правы, можно вернуть List<T>. Но yield return создаёт ленивую последовательность (IEnumerable), которая вычисляется «на лету». Это экономит память и время, когда данных много или они генерируются сложно.

// Пример: бесконечная последовательность чисел Фибоначчи
public static IEnumerable<int> Fibonacci(int count)
{
    int prev = 0, current = 1;
    for (int i = 0; i < count; i++)
    {
        yield return prev; // возвращаем и "засыпаем"
        (prev, current) = (current, prev + current);
    }
}

// Использование: вычисляем только первые 10 чисел
foreach (var num in Fibonacci(10))
    Console.WriteLine(num); // 0,1,1,2,3,5,8,13,21,34

Без yield пришлось бы хранить весь список в памяти. А здесь — каждый элемент вычисляется при итерации. Плюс поддерживаются такие LINQ-операции как .Where(), .Select() — они тоже ленивые!

2. Extension Methods — методы-призраки

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

// Создаём метод для строк
public static class StringExtensions
{
    public static bool IsPalindrome(this string str)
    {
        if (string.IsNullOrEmpty(str)) return false;
        var reversed = new string(str.Reverse().ToArray());
        return str.Equals(reversed, StringComparison.OrdinalIgnoreCase);
    }
}

// Теперь можно писать:
string word = "А роза упала на лапу Азора";
Console.WriteLine(word.IsPalindrome()); // True (если убрать пробелы)

Важно: extension-методы не могут переопределять существующие методы, они лишь «тень». Но это идеальный способ добавить функциональность к DateTime, string или даже к интерфейсам (например, IEnumerable<T>).

3. Выполнение кода до Main() — не миф, а статические конструкторы

Да, такое возможно! Статический конструктор класса выполняется автоматически при первом обращении к классу. Если поместить его в класс с точкой входа, он сработает до Main().

class Program
{
    static Program()
    {
        Console.WriteLine("Я выполняюсь ДО Main()!");
        Environment.Exit(0); // Можно даже завершить программу
    }

    static void Main(string[] args)
    {
        Console.WriteLine("Это не выполнится");
    }
}
// Вывод: "Я выполняюсь ДО Main()!"

Более практичный пример — инициализация глобальных настроек или логирование старта. Но будьте осторожны: статические конструкторы могут замедлить первый запуск.

4. Контекстные ключевые слова (C# 6+)

Некоторые слова становятся ключевыми только в определённом контексте. Например:

  • async/await — для асинхронного кода (но это уже база).
  • var — неявная типизация (работает только для локальных переменных).
  • from/where/select — в LINQ-запросах.
  • partial — разделение класса на несколько файлов.

5. Оператор switch с кортежами (C# 8+)

// Сравнение двух значений без if-else
string result = (a, b) switch
{
    (> 0, > 0) => "Оба положительные",
    (< 0, < 0) => "Оба отрицательные",
    (_, 0) => "Второй ноль",
    _ => "Остальные случаи"
};

Совет: не пытайтесь использовать все фишки сразу. Начните с yield return и extension-методов — они реально упрощают код. А static constructors используйте только когда точно знаете, зачем.

<<>>

Альтернативный взгляд: фишки, о которых молчат даже в документации

Добавлю несколько менее известных, но очень полезных возможностей:

1. Null-conditional operators (?.) и null-coalescing assignment (??=)

// Безопасный доступ: если obj == null, то result = null
int? length = obj?.Name?.Length;

// Присвоение по умолчанию, если null
list ??= new List<string>(); // C# 8+

2. Stackalloc в безопасном коде (C# 7.2+)

Можно выделять память на стеке без unsafe-блоков:

Span<int> numbers = stackalloc int[10]; // Быстрее, чем new int[10]

3. Именованные аргументы и кортежи с именами

// Именованные аргументы делают код самодокументируемым
var result = Calculate(salary: 50000, taxRate: 0.13);

// Кортежи с именами полей (C# 7+)
var person = (Name: "Анна", Age: 30);
Console.WriteLine(person.Name); // "Анна"

4. Атрибут [CallerMemberName] (C# 5+)

Автоматически подставляет имя вызывающего метода — идеально для INotifyPropertyChanged:

void SetProperty<T>(ref T field, T value, [CallerMemberName] string prop = "")
{
    field = value;
    OnPropertyChanged(prop);
}

Главное правило: используйте фишки, которые делают код читаемее, а не короче ради спортивного интереса. И всегда помните — «скрытые» возможности часто становятся явными в следующей версии C#!