День 1408. #ЗаметкиНаПолях #TipsAndTricks
Современные Приёмы в C#. Часть 3. Окончание
Часть 1
Часть 2
Часть 3. Начало
Пример применения
В кодовой базе бывают области, где определённые аргументы метода всегда являются константами. Обычно это указывает на то, что метод следует разделить на два, но это не всегда возможно. Возьмём популярный костыль с булевым аргументом, отвечающим за синхронное или асинхронное исполнение метода:
private async Task<string> GetCoreAsync(bool sync)
{
if (sync)
Thread.Sleep(TimeSpan.FromSeconds(1));
else
await Task.Delay(TimeSpan.FromSeconds(1));
return "Hi!";
}
public string Get() =>
GetCoreAsync(true).GetAwaiter().GetResult();
public Task<string> GetAsync() =>
GetCoreAsync(false);
Допустим, что GetCoreAsync намного длиннее и сложнее, и создание двух разных методов действительно усложнит обслуживание.
Воспользуемся обобщённой генерацией кода для создания двух разных методов:
Извлечём различия в коде (Thread.Sleep и Task.Delay). Для них потребуется определение в интерфейсе, и они будут реализованы для каждого типа-значения. Поскольку код может быть синхронным или асинхронным, мы будем использовать ValueTask в качестве типа результата. GetCoreAsync может просто вызывать эти методы интерфейса:
private interface IDelay
{
static abstract ValueTask
DelayAsync(TimeSpan delay);
}
private readonly struct SyncDelay : IDelay
{
public static ValueTask
DelayAsync(TimeSpan delay)
{
Thread.Sleep(delay);
return new();
}
}
private readonly struct AsyncDelay : IDelay
{
public static async ValueTask
DelayAsync(TimeSpan delay) =>
await Task.Delay(delay);
}
private async Task<string> GetCoreAsync<TDelay>()
where TDelay: struct, IDelay
{
await TDelay.DelayAsync(TimeSpan.FromSeconds(1));
return "Hi!";
}
public string Get() =>
GetCoreAsync<SyncDelay>()
.GetAwaiter().GetResult();
public Task<string> GetAsync() =>
GetCoreAsync<AsyncDelay>();
Основная реализация (GetCoreAsync) упрощена и более очевидна. Публичный интерфейс (Get и GetAsync) вообще не изменился. И во время выполнения, если используется только один путь, то только один путь будет скомпилирован JIT-компилятором. Если используются оба пути, JIT создаст две копии GetCoreAsync, каждая из которых оптимизирована для своей ситуации (асинхронной или синхронной). Это особенно полезный метод для библиотек, которым может потребоваться предоставлять обе формы методов, но существует высокая вероятность использования только одного из них.
Стивен Тауб рассказывает в недавнем посте, как BCL использует эту технику.
Примечания и Ограничения
Используя дженерики C# с типами-значений, мы можем обеспечить мономорфизацию, однако остальная часть поведения не гарантируется. JIT не гарантирует, что какие-либо конкретные методы будут встроены или что произойдет какая-либо оптимизация. Разумно предположить, что она будет иметь место, а при современной многоуровневой оптимизации можно также ожидать, что метод станет более оптимизированным, если вызывается много раз.
Вы ограничены только тем, что может быть выражено в виде членов интерфейса. Вы можете определять методы и свойства, но не константные значения или вложенные типы.
Источник: https://blog.stephencleary.com/2022/10/modern-csharp-techniques-3-generic-code-generation.html