Как инициализировать статическую переменную в базовом классе в контексте класса-наследника с использованием CRTP

Решил переделать ООП реализация предметов в комнате с использованием CRTP.

using System;
using System.Linq;
using System.Reflection;
using System.Collections;
using System.Runtime.CompilerServices;
[AttributeUsage(AttributeTargets.Method)]
public class ActionItemAttribute : Attribute {
    public string ActionName { get; set; }
    public string ActionDescription { get; set; }
}
// если сделать реализацию немножко по-другому, то без этого класса можно обойтись
public class ActionItem {
    public IList<CustomAttributeNamedArgument> Attributes { get; init; }
    public string MethodName { get; init; }
    public ActionItem(string mn, IList<CustomAttributeNamedArgument> il) { MethodName = mn; Attributes = il; }
}
class ItemBase {
    public string Name { get; init; }
    public ItemBase(string nm) => Name = nm;
    // это вспомогательная функция, можно и без неё
    protected string GetActionDescription([CallerMemberName] string callingMethod = "")
        => (GetType().GetMethod(callingMethod)!.GetCustomAttribute(typeof(ActionItemAttribute)) as ActionItemAttribute)
            ?.ActionDescription!;
    // это нужно
    public virtual List<ActionItem> ActionableItems {get => null; }
    // это нужно
    public object Act(string action) {
        Console.Write($"    {((ActionItemAttribute)Attribute.GetCustomAttribute(GetType().GetMethod(action), typeof(ActionItemAttribute), true)).ActionName}: ");
        if (GetType().GetMethod(action).ReturnType != typeof(void))
            return GetType().GetMethod(action).Invoke(this, new object[] { });
        else { GetType().GetMethod(action).Invoke(this, new object[] { }); return null; }
    }
    // каждое действие должно быть помечено таки атрибутом, он может быть и пустой,
    // если дополнительная информация не нужна
    [ActionItem(ActionName = "Взять", ActionDescription = "Взять предмет")]
    public virtual void Take() => Console.WriteLine("Не забудь вернуть обратно.");
    [ActionItem(ActionName = "Вытащить содержимое", ActionDescription = "Вытащить содержимое из предмета")]
    public virtual object GetContent() { Console.WriteLine("Аааа, ломать не нужно!!!"); return null; }
}
class Item<TSelf> : ItemBase where TSelf : Item<TSelf> {
    protected static List<ActionItem> _sactionlist;
    // это нужно для построения спика действий
    protected static List<ActionItem> GetActionList(Type T) =>
        T.GetMethods().Where(w => w.CustomAttributes
                .Where(w => w.AttributeType == typeof(ActionItemAttribute)).Any())
            .Select(s => new ActionItem(s.Name, s.CustomAttributes
                .Single(s => s.AttributeType == typeof(ActionItemAttribute)).NamedArguments))
            .ToList();
    public override List<ActionItem> ActionableItems { get => _sactionlist; }
    public Item(string nm) : base(nm) { }
    // это вспомогательная функция, можно и без неё
}
class WindowItem : Item<WindowItem> {
    static WindowItem() => _sactionlist = GetActionList(MethodBase.GetCurrentMethod().DeclaringType);
    public WindowItem(string name) : base(name) { }
    [ActionItem(ActionName = "Открыть", ActionDescription = "Открыть окно")]
    public void Open() => Console.WriteLine(GetActionDescription());
    [ActionItem(ActionName = "Закрыть", ActionDescription = "Закрыть окно")]
    public void Close() => Console.WriteLine(GetActionDescription());
    [ActionItem(ActionName = "Взять", ActionDescription = "Взять предмет")]
    public override void Take() => Console.WriteLine("Ага, попробуй возьми.");
}
class Cloth : Item<Cloth> {
    static Cloth() => _sactionlist = GetActionList(MethodBase.GetCurrentMethod().DeclaringType);
    public Cloth(string name) : base(name) { }
    public void Wash() => Console.WriteLine("Постирать тряпку.");
}
class Box : Item<Box> {
    static Box() => _sactionlist = GetActionList(MethodBase.GetCurrentMethod().DeclaringType);
    public Box(string name) : base(name) { }
    [ActionItem(ActionName = "Вытащить содержимое", ActionDescription = "Вытащить содержимое из коробки")]
    public override object GetContent() => new object[] { "большие солнечные очки", "звонок от велосипеда", "вобла" };
}
class Room : IEnumerable {
    List<ItemBase> _items;
    public Room() => _items = new List<ItemBase>();
    public void Add(ItemBase i) => _items.Add(i);
    public IEnumerator GetEnumerator() => _items.GetEnumerator();
}
internal class Program {
    static void ConsoleWriteLine(object o) =>
        Console.WriteLine(o is IEnumerable ? String.Join("; ", (object[])o) : o);
    static void Main() {
        var room = new Room { new WindowItem("Переднее окно"), new Cloth("Тряпка для доски"), new Box("Шкатулка") };
        foreach (ItemBase itm in room) {
            Console.WriteLine($"Предмет: {itm.Name}");
            foreach (var method in itm.ActionableItems)
                if (itm.GetType().GetMethod(method.MethodName).ReturnType != typeof(void))
                    ConsoleWriteLine(itm.Act(method.MethodName));
                else itm.Act(method.MethodName);
        }
    }
}

Перенёс поле _sactionlist в базовый класс, но не получается избавиться от статических конструкторов (static WindowItem, static Cloth, static Box) в каждом наследнике. Есть какой-то выход?


Ответы (1 шт):

Автор решения: VladD

Поскольку мы в CRTP знаем «настоящий» тип производного класса, то вот так должно работать:

class Item<TSelf> : ItemBase where TSelf : Item<TSelf> {
    protected static List<ActionItem> _sactionlist = GetActionList(typeof(TSelf));
    // ...
}

class Cloth : Item<Cloth> {
    public Cloth(string name) : base(name) { }
    public void Wash() => Console.WriteLine("Постирать тряпку.");
}
→ Ссылка