Утиная Типизация в C# с Помощью Перехватчиков. Часть 2
Некоторое время назад я выложил пост от Steven Giesel о реализации утиной типизации в C# с помощью перехватчиков (interceptors). Тогда это был лишь прототип с множеством недоработок. Теперь же это полноценная библиотека: IfItQuacks.
TL;DR
Объявите интерфейс, пометьте метод атрибутом
[DuckTyped] и передавайте в него любой подходящий объект — без базового класса, без явной реализации интерфейса и без каких-либо атрибутов на самих типах:using IfItQuacks;
public interface INamed
{
string Name { get; }
}
public class Person { public string Name => "Steven"; }
public class Mallard { public string Name => "Donald"; }
public partial class Greeter
{
[DuckTyped]
public string Greet(
INamed first,
INamed second,
string greeting = "Hello")
=> $"{greeting}, {first.Name} and {second.Name}!";
}
Console.WriteLine(new Greeter()
.Greet(new Person(), new Mallard()));
// Hello, Steven and Donald!
Ни
Person, ни Mallard не знают об INamed. Генератор кода на этапе компиляции проверяет наличие у обоих типов свойства Name и перенаправляет вызов через небольшой сгенерированный адаптер. Никакой рефлексии, никакого dynamic. Если типы несовместимы, анализатор выдаст ошибку.Можно также явно привести объекты к интерфейсу, чтобы хранить их:
List<INamed> names = [
Duck.As<INamed>(new Person()),
Duck.As<INamed>(new Mallard())
];
Подробности можно найти в документации. Далее кратко основные моменты.
Анонимные типы — это «утки»
Анонимные типы отлично «крякают» (по сути, то же самое, что предлагает TypeScript):
new Greeter().Greet(
new { Name = "Steven" },
new { Name = "Donald" });
По сути, можно писать свои тесты с «мгновенными моками»:
public interface IPerson
{
string Name { get; }
int Age { get; }
}
public partial class AgeChecker
{
[DuckTyped]
public bool IsAdult(IPerson person)
=> person.Age >= 18;
}
[Test]
public void Should_detect_adults()
{
var checker = new AgeChecker();
Assert.That(
checker.IsAdult(new { Name = "Steven", Age = 90 }), Is.True);
Assert.That(
checker.IsAdult(new { Name = "Baby Duck", Age = 1 }), Is.False);
}
Либо:
IPerson stub =
Duck.As<IPerson>(new { Name = "Donald", Age = 90 });
List<IPerson> family =
[
Duck.As<IPerson>(new { Name = "Huey", Age = 10 }),
Duck.As<IPerson>(new { Name = "Dewey", Age = 10 }),
Duck.As<IPerson>(new { Name = "Louie", Age = 10 }),
];
Идентичность сохраняется
Адаптеры перенаправляют вызовы методов
Equals, GetHashCode и ToString к обёрнутому экземпляру, благодаря чему они корректно работают в словарях и множествах. Кроме того, можно получить доступ к исходному объекту:var person = new Person();
Duck.As<INamed>(person)
.Equals(Duck.As<INamed>(person)); // true
Duck
.Unwrap(Duck.As<INamed>(person)) is Person; // true
А также поддерживаются:
- Любой интерфейс, включая интерфейсы фреймворков:
Duck.As<IDisposable>(x), параметры IEnumerable<T> и т.п.;- Параметры интерфейса, не требующие утиной типизации (например,
ILogger), продолжают принимать реализации, значения null и значения по умолчанию;- Обобщённые интерфейсы (
IContainer<T>) и обобщённые методы [DuckTyped] с выведением аргументов типа;- События, индексаторы и реализация интерфейса по умолчанию;
- Делегаты, лямбда-выражения и группы методов для интерфейсов с одним методом;
- Статические абстрактные члены и операторы с помощью ограничений утиной типизации (
where T : IAddable<T>) - включая обобщённую математику;- Методы экземпляра и статические методы, ref/out, params, значения по умолчанию и именованные аргументы;
- Нулевая настройка: установите пакет, никаких изменений в файлах проекта не требуется.
Источник: https://steven-giesel.com/blogPost/41599817-4457-441e-b874-1a5c67f4e7cc/if-it-quacks-part-2