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# 强制转换是有成本的,我不想在每个侵入式集合实现中传播此强制转换...
I'm currently working on a simple way to implement a intrusive tree structure in C#. As I'm mainly a C++ programmer, I immediatly wanted to use CRTP. Here is my code:
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;
}
This works but... I can't understand why I have to cast when calling a_node.SetParent((T)this), as I'm using generic type restriction...
C# cast has a cost, and I'd like not to spread this cast in each intrusive collection implementation...
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(5)
这至少是 TreeNode 类型。它可以是派生的,也可以是 TreeNode。 SetParent 需要一个 T。但 T 可以是与此不同的类型。我们知道 this 和 T 都派生自 TreeNode 但它们可以是不同的类型。
例子:
this is at least of type TreeNode. It could be derived or it could be exactly TreeNode. SetParent expects a T. But T can be a different type than this is of. We know that this and T both derive from TreeNode but they can be different types.
Example:
没有人保证
T
和this
的类型相同。它们甚至可以是 TreeNode 的不相关子类。您希望在奇怪的重复模板模式中使用
T
,但通用约束无法表达这一点。愚蠢的实现可以定义为StupidNode:TreeNode。
Nobody guaranteed that
T
and the type ofthis
are the same. They can even be unrelated subclasses ofTreeNode
.You expect
T
to be used in the curiously recurring template pattern, but generic constraints can't express that.A stupid implementation could be defined as
StupidNode:TreeNode<OtherNode>
.问题出在这一行:
T 作为 TreeNode 是一个递归定义,它无法在预编译时确定,甚至无法静态检查。不要使用模板,或者如果使用,则需要重构&将节点与有效负载分开(即节点数据与节点本身)。
此外,我不确定您为什么要调用 a_node.SetParent(this)。看来 AddChild 更适合命名为 SetParent,因为您将此实例设置为 a_node 的父级。可能是我不熟悉一些深奥的算法,否则看起来就不对劲。
The problem is with this line:
T being a TreeNode is a recursive definition it can't be determined pre-compile or even statically checked. Do not use template, or if you do you need to refactor & separate the node from the payload (Ie the node data from the node itself.)
Also I'm not sure why you're calling a_node.SetParent(this). It seems like AddChild is more aptly named SetParent, because you're setting this instance as the parent of a_node. May be it's some esoteric algorithm I'm not familiar with, otherwise it doesn't look right.
考虑一下如果我们偏离 CRTP 约定,编写...
...然后按如下方式调用上面的代码,会发生什么:
AddChild
调用抛出一个InvalidCastException
并表示无法将“Bar”类型的对象强制转换为“Foo”类型。
关于 CRTP 习惯用法 - 这是单独的约定,要求泛型类型与声明类型相同。该语言必须支持不遵循 CRTP 约定的其他情况。 Eric Lippert 就这个主题写了一篇很棒的博客文章,他从另一个 crtp via c# 答案 链接到该文章。
综上所述,如果您将实现更改为这样...
...之前引发
InvalidCastException
的上述代码现在可以工作。此更改使m_Parent
成为TreeNode
类型;使this
要么是Foo
类中的T
类型,要么是TreeNode
的子类Bar
类的情况,因为Bar
继承自TreeNode
- 无论哪种方式都允许我们省略SetParent
中的强制转换> 并通过该省略避免无效的强制转换例外,因为分配在所有情况下都是合法的。这样做的代价是不再能够像以前那样在所有地方自由使用T
,这牺牲了 CRTP 的大部分价值。我的一位同事/朋友认为自己是某种语言/语言功能的新手,直到他可以诚实地说他“在愤怒中使用了它”;也就是说,他对这种语言足够熟悉,以至于他会因为无法实现他所需要的东西或者这样做很痛苦而感到沮丧。这很可能是其中一种情况,因为这里存在一些限制和差异,与以下事实相呼应:泛型不是模板。
Consider what happens if we deviate from CRTP convention by writing...
...and then call the above code as follows:
The
AddChild
call throws anInvalidCastException
sayingUnable to cast object of type 'Bar' to type 'Foo'.
Regarding the CRTP idiom - it is convention alone requiring the generic type to be the same as the declaring type. The language must support the other cases where CRTP convention is not followed. Eric Lippert wrote a great blog post on this topic, that he linked from this other crtp via c# answer.
All of that said, if you change the implementation to this...
...the above code that previously threw the
InvalidCastException
now works. The change makesm_Parent
a type ofTreeNode<T>
; makingthis
either the typeT
as in theFoo
class' case or a subclass ofTreeNode<T>
in theBar
class case sinceBar
inherits fromTreeNode<Foo>
- either way allows us to omit the cast inSetParent
and by that omission avoid the invalid cast exception since the assignment is legal in all cases. The cost of doing this is no longer being able to freely useT
in all places as it had previously been used which sacrifices much of the value of CRTP.A colleague/friend of mine considers himself a newbie to a language/language-feature until he can honestly say that he's "used it in anger;" that is, he knows the language well enough to be frustrated that there is either no way of accomplishing what he needs or that doing so is painful. This very well could be one of those cases, as there are limitations and differences here that echo the truth that generics are not templates.
当您使用引用类型时,并且您知道沿着类型层次结构进行的强制转换将会成功(此处没有自定义强制转换),则无需实际强制转换任何内容。引用整数的值在强制转换之前和之后是相同的,那么为什么不直接跳过强制转换呢?
这意味着你可以在CIL/MSIL中编写这个令人鄙视的AddChild方法。方法主体操作码如下:
.NET 根本不会关心您是否没有转换该值。 The Jitter 似乎只关心商店的大小是否一致,它们始终仅供参考。
加载 Visual Studio 的 IL 支持扩展(可能必须打开 vsix 文件并修改支持的版本),并使用 MethodImpl.ForwardRef 属性将 C# 方法声明为 extern。然后只需在 .il 文件中重新声明该类并添加您需要的一种方法实现,其主体已在上面提供。
请注意,这还会手动将 SetParent 方法内联到 AddChild 中。
When you are working with reference types, and you know for a fact that your cast along the type hierarchy will succeed (no custom casting here), then there is no need to actually cast anything. The value of the reference integer is the same before and after the cast, so why not just skip the cast?
That means you can write this despised AddChild method in CIL/MSIL. The method body opcodes are as follows:
.NET will not care at all that you did not cast the value. The Jitter seems to only care about the size of stores being consistent, which they always are for references.
Load up the IL Support extension for Visual Studio (may have to open the vsix file and modify supported version) and declare the C# method as extern with MethodImpl.ForwardRef attribute. Then just re-declare the class in a .il file and add the one method implementation you need, the body of which is provided above.
Note that this also manually inlines your SetParent method into AddChild.