TGStat
TGStat
Type to search
Advanced channel search
  • flag English
    Site language
    flag Russian flag English flag Uzbek
  • Sign In
  • Catalog
    Channels and groups catalog Regional compilations Thematic compilations Платные каналы Search for channels
    Add a channel/group
  • Ratings
    Rating of channels Rating of groups Posts rating
    Ratings of brands and people
  • Analytics
  • Search by posts
  • Telegram monitoring
  • Promotion
    Advertising through Yandex Business Advertising in channels through TGStat Agency Advertising on TGStat.ru website
Чат .NET Разработчиков

14 Sep, 08:00

Open in Telegram Share Report

Forward from: .NET Разработчик
День 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
Catalog
Channels and groups catalog Channels compilations Search for channels Add a channel/group
Ratings
Rating of Telegram channels Rating of Telegram groups Posts rating Ratings of brands and people
API
API statistics Search API of posts API Callback
Our channels
@TGStat @TGStat_Chat @telepulse @TGStatAPI
Read
Академия TGStat Telegram Research 2019 Telegram Research 2021 Telegram Research 2023
Contacts
Справочный центр Support Email Jobs
Miscellaneous
Terms and conditions Privacy policy Public offer
Our bots
@TGStat_Bot @SearcheeBot @TGAlertsBot @tg_analytics_bot @TGStatChatBot