TGStat
TGStat
Введите текст для поиска
Расширенный поиск каналов
  • flag Russian
    Язык сайта
    flag Russian flag English flag Uzbek
  • Вход на сайт
  • Каталог
    Каталог каналов и чатов Региональные подборки Тематические подборки Платные каналы Поиск каналов
    Добавить канал/чат
  • Рейтинги
    Рейтинг каналов Рейтинг чатов Рейтинг публикаций
    Рейтинги брендов и персон
  • Аналитика
  • Поиск по публикациям
  • Мониторинг Telegram
  • Продвижение
    Реклама через Яндекс Бизнес Реклама в каналах через TGStat Agency Реклама на сайте TGStat.ru
.NET Разработчик

14 Sep, 08:00

Открыть в Telegram Поделиться Пожаловаться

День 2784. #Оффтоп
Утиная Типизация в C# с Помощью Перехватчиков
Если это ходит как утка и крякает как утка — значит, это утка. Применим утиную типизацию и заставим это работать в C# с помощью перехватчиков!

Примечание: этот пост написан в образовательных целях, но вы смело можете использовать описанный подход в проде 😉

В TypeScript можно сделать вот так:
class A {
Do(): void { console.log("A.Do"); }
}

class B {
Do(): void { console.log("B.Do"); }
}

function foo(a: { Do(): void }) {
a.Do();
}

Попробуем сделать что-то подобное в C#:
public class A
{
public void Do() => Console.WriteLine("A.Do");
}
public class B
{
public void Do() => Console.WriteLine("B.Do");
}

public void Foo(??? a) => a.Do();
У классов A и B нет ничего общего: ни базового класса, ни общего интерфейса. Нужно придумать некую «форму» (обозначим её как ???) — нечто такое, что позволит успешно скомпилировать вызовы Foo(new A()) и Foo(new B()) и обеспечит вызов нужного метода Do() в каждом случае, при этом вообще не затрагивая сами классы A и B.

Кроме того, запрещено использовать следующие подходы:
- dynamic (хотя это и невероятно крутая штука);
- аргумент типа object с последующим приведением типов через is или as;
- модификацию классов A или B (например, добавление общего базового класса или интерфейса).

Перехватчики
Начиная с .NET 8, в C# появилась экспериментальная функция компилятора — перехватчики. Генератор исходного кода может создать метод, пометить его атрибутом [InterceptsLocation], указав конкретное место вызова в вашем коде, и компилятор незаметно перенаправит вызов на этот метод. Это происходит без каких-либо затрат ресурсов во время выполнения, так как всё разрешается на этапе компиляции.

Обычно это реализуется через сочетание пользовательских атрибутов (например, DuckType и DuckShape), создаваемых генератором кода, и логики перехватчика: система находит все места вызовов и выполняет необходимые действия.

Таким образом, с точки зрения использования нам всё равно потребуется нечто вроде интерфейса (или свойства) для «описания формы»:
[DuckShape]
public interface IDoable { void Do(); }

public static partial class Ops
{
[DuckTyped]
public static void Foo(IDoable a) => a.Do();
}
Вы пишете Foo(IDoable a) => a.Do() - именно так, как вам хочется. Атрибут [DuckShape] помечает IDoable как структурный контракт, а не как интерфейс, который нужно реализовывать вручную. Атрибут [DuckTyped] сообщает генератору: «Вот настоящая логика; сделай так, чтобы её можно было вызывать с любым объектом, подходящим по структуре».

Полный код генератора/перехватчика на GitHub, а здесь рассмотрим наиболее интересные фрагменты.

Так или иначе, каждый вызов сводится к следующей обобщённой конструкции:
[InterceptsLocation(1, "…")]
public static void Interceptor_1(A value)
{
Ops.Foo((IDoable)(new ShapeAdapter_IDoable_A(value)));
}
ShapeAdapter_IDoable_A - это небольшая генерируемая структура только для чтения, которая реализует IDoable, перенаправляя сразу в A:
internal readonly struct
ShapeAdapter_IDoable_A : IDoable
{
private readonly A _value;
public ShapeAdapter_IDoable_A(A value)
=> _value = value;
public void Do() => _value.Do();
}
Вот и весь фокус. Это ведёт к незначительным накладным расходам (возможно инлайнинг поможет с этим). Таким образом, следующий код работает прекрасно:
Ops.Foo(new A()); // "A.Do"
Ops.Foo(new B()); // "B.Do"
Это также работает для свойств:
[DuckShape]
public interface INameable
{ string Name { get; set; } }

public class Person
{ public string Name { get; set; } = ""; }

[DuckTyped]
public static string Greet(INameable n)
=> $"Hello, {n.Name}!";

Greet(new Person { Name = "Jon" }); // "Hello, Jon!"

Это можно оформить в NuGet, если кто-то увидит в этом реальный смысл.

Итого
Полезно ли это? Вероятно, нет — но не в этом главная цель. Хотя язык не поддерживает это напрямую, подобное поведение можно в определённой мере «эмулировать».

Источник:
https://steven-giesel.com/blogPost/5170e165-29e8-437a-b5ce-446a84943809/quack-quack-ducktyping-in-c-with-interceptors

1.4k 2 10 10 7
Каталог
Каталог каналов и чатов Подборки каналов Поиск каналов Добавить канал/чат
Рейтинги
Рейтинг каналов Telegram Рейтинг чатов Telegram Рейтинг публикаций Рейтинги брендов и персон
API
API статистики API поиска публикаций API Callback
Наши каналы
@TGStat @TGStat_Chat @telepulse @TGStatAPI
Почитать
Академия TGStat Исследование Telegram 2019 Исследование Telegram 2021 Исследование Telegram 2023
Контакты
Справочный центр Поддержка Почта Вакансии
Всякая всячина
Пользовательское соглашение Политика конфиденциальности Публичная оферта
Наши боты
@TGStat_Bot @SearcheeBot @TGAlertsBot @tg_analytics_bot @TGStatChatBot