C# - Интрузивная древовидная структура, использующая CRTP


В настоящее время я работаю над простым способом реализации интрузивной древовидной структуры в C#. Поскольку я в основном программист на C++, я сразу же захотел использовать CRTP. Вот мой код:

public class TreeNode<T> where T : TreeNode<T>
{
    public void AddChild(T a_node)
    {
        a_node.SetParent((T)this); // This is the part I hate
    }

    void SetParent(T a_parent)
    {
        m_parent = a_parent;
    }

    T m_parent;
}

Это работает, но... Я не могу понять, почему я должен бросать при вызове a_node.SetParent ((T)this), поскольку я использую ограничение универсального типа... Приведение C# имеет стоимость,и я не хотел бы распространять это приведение в каждой реализации навязчивой коллекции...

5 5

5 ответов:

Это, по крайней мере, тип TreeNode. Это может быть производное или это может быть именно Тринод. SetParent ожидает T, но T может быть другого типа,чем этот. Мы знаем, что это и T оба происходят от TreeNode, но они могут быть разными типами.

Пример:

class A : TreeNode<A> { }
new TreeNode<A>() //'this' is of type 'TreeNode<A>' but required is type 'A'

Никто не гарантировал, что T и тип this одинаковы. Они могут быть даже несвязанными подклассами TreeNode.

Вы ожидаете, что T будет использоваться в любопытно повторяющемся шаблоне шаблона,но общие ограничения не могут выразить это.

Глупая реализация может быть определена как StupidNode:TreeNode<OtherNode>.

Проблема с этой строкой:

 TreeNode<T> where T : TreeNode<T>

T будучи TreeNode является рекурсивным определением, оно не может быть определено до компиляции или даже статически проверено. Не используйте шаблон, или если вам нужно выполнить рефакторинг и отделить узел от полезной нагрузки (т. е. данные узла от самого узла.)

 public class TreeNode<TPayload>
 {
     TPayload NodeStateInfo{get;set;}

     public void AddChild(TreeNode<TPayload> a_node)
     {
         a_node.SetParent(this); // This is the part I hate
     }

     void SetParent(TreeNode<TPayload> a_parent)
     {
     }
 }

Также я не уверен, почему вы вызываете a_node.SetParent (это). Похоже, что AddChild более точно назван SetParent, потому что вы устанавливаете этот экземпляр в качестве родителя a_node. Май может быть, это какой-то эзотерический алгоритм, с которым я не знаком, иначе он не выглядит правильным.

Рассмотрим, что произойдет, если мы отступим от соглашения CRTP в письменной форме...

public class Foo : TreeNode<Foo>
{
}

public class Bar : TreeNode<Foo> // parting from convention
{
}

...а затем вызовите приведенный выше код следующим образом:

var foo = new Foo();
var foobar = new Bar();
foobar.AddChild(foo);

Вызов AddChild вызывает InvalidCastException высказывание Unable to cast object of type 'Bar' to type 'Foo'.

Что касается идиомы CRTP-это только соглашение, требующее, чтобы универсальный тип был таким же, как и тип объявления. Язык должен поддерживать другие случаи, когда конвенция CRTP не соблюдается. Эрик Липперт написал отличный пост в блоге на эту тему, который он связал с этим другим crtp через C# ответ .

Все это сказано, Если вы измените реализацию на это...

public class TreeNode<T> where T : TreeNode<T>
{
    public void AddChild(T a_node)
    {
        a_node.SetParent(this);
    }

    void SetParent(TreeNode<T> a_parent)
    {
        m_parent = a_parent;
    }

    TreeNode<T> m_parent;
}

...приведенный выше код, который ранее бросал InvalidCastException, теперь работает. Это изменение делает m_Parent типом TreeNode<T>; делая this либо типом T, как в случае Foo класса, либо подклассом TreeNode<T> в случае Bar класса, так как Bar наследует от TreeNode<Foo> - в любом случае позволяет нам опустить приведение в SetParent и этим упущением избежать недопустимого исключения приведения, так как назначение является законным во всех случаях. Стоимость этого больше не может свободно использовать T во всех местах, как это было раньше, что приносит в жертву большую часть стоимости CRTP.

Мой коллега / друг считает себя новичком в языке / языковой особенности до тех пор, пока он не сможет честно сказать, что он "использовал ее в гневе"; то есть он знает язык достаточно хорошо, чтобы быть разочарованным тем, что нет никакого способа выполнить то, что ему нужно, или что делать это больно. Этот очень хорошо, что это один из таких случаев, поскольку здесь есть ограничения и различия, которые повторяют истину о том, что дженерики не являются шаблонами.

Когда вы работаете со ссылочными типами, и вы точно знаете, что приведение по иерархии типов будет успешным (здесь нет пользовательского приведения), тогда нет необходимости что-либо приводить. Значение ссылочного целого числа одинаково до и после приведения, так почему бы просто не пропустить приведение?

Это означает, что вы можете написать этот презираемый метод AddChild в CIL/MSIL. Опкоды тела метода следующие:
ldarg.1
ldarg.0
stfld TreeNode<class T>::m_parent
ret

.NET вообще не будет волновать, что вы не бросили значение. Дрожание, кажется, заботится только о том, чтобы размер магазинов был последовательным, что они всегда делают для ссылок.

Загрузите расширение поддержки IL для Visual Studio (возможно, потребуется открыть файл vsix и изменить поддерживаемую версию) и объявите метод C# как extern с помощью MethodImpl.Атрибут ForwardRef. Затем просто повторно объявите класс в файле. il и добавьте одну необходимую вам реализацию метода, тело которой приведено выше.

Обратите внимание, что это также вручную inlines ваш метод SetParent в AddChild.