tintoy
tintoy

Reputation: 605

C# generics - why is an explicit cast needed when dealing with type parameters in this way?

me again. Sorry about this.

Following on from my previous question (I think I failed to adequately demonstrate the source of my confusion), here is the actual function I was writing:

/// <summary>
///     A b-tree node.
/// </summary>
public class BTreeNode
{
    /// <summary>
    ///     Create a new b-tree node.
    /// </summary>
    public BTreeNode()
    {

    }

    /// <summary>
    ///     The node name.
    /// </summary>
    public string Name
    {
        get;
        set;
    }

    /// <summary>
    ///     The left-hand child node.
    /// </summary>
    public BTreeNode Left
    {
        get;
        set;
    }

    /// <summary>
    ///     The right-hand child node.
    /// </summary>
    public BTreeNode Right
    {
        get;
        set;
    }
}

/// <summary>
///     Perform a breadth-first traversal of a b-tree.
/// </summary>
/// <param name="rootNode">
///     The b-tree's root node.
/// </param>
/// <param name="forEachNode">
///     An action delegate to be called once for each node as it is traversed.
///     Also includes the node depth (0-based).
/// </param>
public static void TraverseBreadthFirst<TNode>(this TNode rootNode, Action<TNode, int> forEachNode)
    where TNode : BTreeNode
{
    if (rootNode == null)
        throw new ArgumentNullException("rootNode");

    if (forEachNode == null)
        throw new ArgumentNullException("forEachNode");

    Queue<Tuple<TNode, int>> nodeQueue = new Queue<Tuple<TNode, int>>(3); // Pretty sure there are never more than 3 nodes in the queue.

    nodeQueue.Enqueue(new Tuple<TNode, int>(rootNode, 0));
    while (nodeQueue.Count > 0)
    {
        Tuple<TNode, int> parentNodeWithDepth = nodeQueue.Dequeue();
        TNode parentNode = parentNodeWithDepth.Item1;
        int nodeDepth = parentNodeWithDepth.Item2;

        forEachNode(parentNode, nodeDepth);
        nodeDepth++;

        if (parentNode.Left != null)
            nodeQueue.Enqueue(new Tuple<TNode, int>((TNode)parentNode.Left, nodeDepth));

        if (parentNode.Right != null)
            nodeQueue.Enqueue(new Tuple<TNode, int>((TNode)parentNode.Right, nodeDepth));
    }
}

I am not sure I see why the explicit cast to TNode is needed here:

nodeQueue.Enqueue(new Tuple<TNode, int>((TNode)parentNode.Left, nodeDepth));

Under what circumstances could parentNode.Left be something that is not assignable to TNode (given TNode is constrained to be of type BTreeNode or derived).

To put the question another way, under what circumstances could this function result in an InvalidCastException? If there isn't one, then why does the compiler require an explicit cast?

Edit: I think I'm ok with changing the implementation to something like:

TNode leftNode = parentNode.Left as TNode;
Debug.Assert(leftNode != null || parentNode.Left == null, "Left child is more derived.");
if (leftNode != null)
    nodeQueue.Enqueue(new Tuple<TNode, int>(leftNode, nodeDepth));

Upvotes: 1

Views: 228

Answers (2)

Al Kepp
Al Kepp

Reputation: 5980

Your parentNode.Left is defined as BTreeNode, not as TNode. Even if you derive TNode:BTreeNode, your .Left is still a BTreeNode reference, not a TNode one. So you must cast. You need BTreeNode class to be generic, as Jon Skeet pointed out.

Upvotes: 0

Jon Skeet
Jon Skeet

Reputation: 1500145

parentNode.Left is only typed to be BTreeNode. There's no guarantee that it's the same as TNode. Imagine you had:

class SpecialBTreeNode : BTreeNode
class BoringBTreeNode : BTreeNode

Now consider TraverseBreadthFirst<SpecialBTreeNode>(rootNode, ...). What's to stop rootNode.Left from returning a BoringBTreeNode?

// This is entirely valid...
SpecialBTreeNode special = new SpecialBTreeNode();
special.Left = new BoringBTreeNode();

It sounds like you might want to make BTreeNode itself generic:

public class BTreeNode<T> where T : BTreeNode<T>
{
    public T Left { get; set; }
    public T Right { get; set; }
}

Upvotes: 4

Related Questions