TGViewer
yet another dev yet another dev @yet_another_dev · 382 subscribers
Post #174 424
Утиная типизация в C#

English version.
Читать на сайте.

Думаю, опытные C#-разработчики знают ответ на вопрос: «Что нужно сделать, чтобы можно было перечислить объекты при помощи foreach?»

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

// Этот код компилируется
var obj = new MyType();
foreach (var item in obj);

class MyType {
public IEnumerator GetEnumerator() {
throw new Exception();
}
}


Такое поведение иногда называют утиной типизацией — компилятору неважно, реализует ли класс интерфейс IEnumerable или нет. Главное, чтобы в нём был метод, возвращающий перечислитель.

Аналогичное поведение встречается и при работе с async-await. Чтобы «ожидать» тип, достаточно, чтобы у него был метод GetAwaiter(), возвращающий тип TaskAwaiter или ValueTaskAwaiter. При этом даже не обязательно, чтобы этот метод был внутри самого ожидаемого типа.

 cs
// Этот код тоже компилируется
var obj = new MyType();
await obj;

class MyType {
}

static class MyTypeExtensions {
public static TaskAwaiter GetAwaiter(
this MyType @object) {
throw new Exception();
}
}


Недавно я наткнулся на ещё один случай, который можно считать примером утиной типизации. Начиная с версии 12, в C# появился упрощённый способ инициализации коллекций:

int[] array = [1, 2, 3, 4, 5];
List<int> list = [1, 2, 3, 4, 5];


Этот лаконичный синтаксис работает не со всеми коллекциями. Например, следующий код не скомпилируется:

Queue<int> queue = [1, 2, 3, 4, 5];
Stack<int> stack = [1, 2, 3, 4, 5];


Компилятор ожидает, что у типа коллекции будет метод Add, но в Queue вместо него используется Enqueue, а в Stack — Push. Изменить эти типы мы не можем, поэтому можно сделать следующий финт и помочь компилятору обнаружить метод Add:

public static class CollectionExtensions {

public static void Add<T>(
this Queue<T> collection,
T item) => collection.Enqueue(item);

public static void Add<T>(
this Stack<T> collection,
T item) => collection.Push(item);
}


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

// и этот код компилируется
MyType collection = [1, 2, 3];

class MyType : IEnumerable {
public IEnumerator GetEnumerator() {
throw new Exception();
}
}

static class MyTypeExtensions {
public static void Add(
this MyType o,
int value) { }
}

UPD: В комментариях выяснили, что оператор using может быть использован с ref struct без IDisposable начиная с C# 8.0.

using var obj = new MyType();

ref struct MyType {
public void Dispose() { }
}

UPD2: В комментариях выяснили, что foreach может обойтись без IEnumerator.

// Этот код компилируется
var obj = new MyType();
foreach (var item in obj);

class MyType {
public MyEnumerator GetEnumerator() => new();
}

class MyEnumerator {
public object Current => new();
public bool MoveNext() => false;
}
  • 👍 17
  • ❤ 1
More from @yet_another_dev
  1. Sep 21, 2026Опубликовал вчера ролик в одной запрещённой в России соцсети про то, как сходил на выборы.…
  2. Sep 20, 2026Мы пришли в 7:50 и очередь уже была 🥲 Пообщались с другими людьми. Многие приехали из дру…
  3. Sep 19, 2026Post #383
  4. Sep 18, 2026Последние пару недель на чат нападают боты со спамом (прикрыл стикером). Поэтому чат тепер…
  5. Sep 17, 2026Что интересного в этой статье: 1. Потрачено $120К, а агенты суммарно отработали около 3-х…
  6. Sep 17, 2026В Microsoft переписали рантайм GitHub Copilot с TypeScript на Rust при помощи агентов. Под…
Threads Profile ViewerView any public Threads profile without an account.Open ThreadLook →Writing with AI? Make it sound human.Metric37 rewrites AI drafts so they read naturally. Free AI detector, 1,500 words free.Try Metric37 →