-2

I have two classes: a non-generic base class Node and a generic derived class Node<T>. The nodes build a chained list, each node containing eventually a different data type.

    public class Node{  
      public Node NextNode{get; set;}  
      public static Node<T> WithData<T>(T data) => new Node<T>(data);  
    }  
    public class Node<T>: Node{  
      public T Data{get; set;}  
      public Node(T data) => Data = data;  
    }  

The classes can be used like this:

    var node1 = Node.WithData(73);      // the node contains an int  
    var node2 = Node.WithData("Good luck!");    // the node contains a string  
    node1.NextNode = node2;         // chaining the nodes  
    Notify(node1.Data == 73));  // displays "true" (method Notify() omitted for brevity)  
    Notify(node1.NextNode.GetType());       // displays "Node`1[System.String]"  
    Notify(node1.NextNode.Data == "Good luck!");  
    //compile error: 'Node' does not contain a definition for 'Data'  

The compiler complains, because NextNode is upcasted to Node and is not anymore of type Node<T>, even though GetType() shows the right generic type.

I can solve the problem by downcasting back to Node<T> (type checks omitted for brevity):

   Notify(((Node<string>)node.NextNode).Data);  
   // verbose and the casting type is usually not known at compile time  
    Notify(((dynamic)node.NextNode).Data); // slow, but works fine at runtime
    if(node.NextNode is Node<string> stringNode) Notify(stringNode.Data);  
    else if(node.NextNode is Node<int> intNode) Notify(intNode.Data);  
    // else if... etc.  
    // bad for maintenance, verbose, especially with many data types  

My questions are:

  • Is there a more suitable way to downcast Node to Node<T> (preferably at runtime?

  • Is there a better approach or a better design of the two classes to avoid or automate the downcasting?

2
  • 1
    What is the purpose of this? Why do you want to store different types in different nodes in the same list? Such a design makes type checks almost unavoidable, and is therefore usually best avoided. You usually want data in a structured format, and if multiple different types are allowed in a collection, you usually want to restrict the allowable types by using an interface. Commented Jan 8 at 7:34
  • @JonasH: My goal is to collect data from different measurement devices over socket streams. The data formats are quite different, such as int, double and other value types, but some are more complex user-defined reference types. The data packages coming from a device are collected asynchronously and added to a chained list, which is scheduled for later processing. Commented Jan 8 at 20:25

4 Answers 4

0

Generics are about type safety. They allow you to declare different variants of a type at compile-time. Safety is guaranteed by the compiler at compile-time.

Therefore, generics do not provide an advantage in a dynamic scenario. In a dynamic scenario like yours, where a Node<T> can have different Ts determined at runtime, data typed as object does the job just as well and is easier to manage.

What is the advantage of if (node is Node<string> ns) UseString(ns.Data); over if (node.Data is string s) UseString(s);? None!

Type your data as object. Period!

public class Node (object data) // Primary constructor
{
    public Node Next { get; set; }
    public object Data { get; set; } = data;
}

Note: The primary constructor was introduced in C# 12.0 and can be used in .NET 4.8, if you add a <LangVersion>latest</LangVersion> in a property group of your *.csproj file (it's just syntactic sugar).


I would like to add a thought on the concept of these nodes in general.

Linked together, the nodes represent a collection. The collection should be a class on its own and would handle all the node relates things (like adding and removing nodes or iterating the collection). The collection would be generic and all the nodes would have data of the same generic type.

Dealing with different data types should be delegated to the data itself. It has nothing to do with nodes or linked lists. For this, we would create an independent type hierarchy for the data only. E.g.:

// Non generic base class as common access point.
public abstract class Data
{
    public abstract object Value { get; }

    // Can be called no matter what the concrete derived type will be.
    public abstract void DoSomethingWithValue();

    public override bool Equals(object obj)
        => obj is Data data &&
            EqualityComparer<object>.Default.Equals(Value, data.Value);

    public override int GetHashCode() => HashCode.Combine(Value);

    public override string ToString() => Value?.ToString();
}

public abstract class TypedData<T>(T value) : Data
{
    public override object Value => TypedValue;

    // Introducing a strongly typed value
    public T TypedValue { get; set; } = value;
}

public class StringData(string s) : TypedData<string>(s)
{
    public override void DoSomethingWithValue()
    {
        TypedValue += " World";
    }
}

public class IntData(int i) : TypedData<int>(i)
{
    public override void DoSomethingWithValue()
    {
        TypedValue += 10;
    }
}

With these example classes, the node data should be of type Data. This allows us to call node.Data.DoSomethingWithValue(); no matter whether a StringValue or a IntValue was added to a node.

If we don't know the type of a node's data, we can get the value through the node.Data.Value property as an object.

If we have a data object of a concrete type, we can get or set a strongly typed value through the TypedValue property:

var data = new StringData("Hello");
data.DoSomethingWithValue();
string s = data.TypedValue;
data.TypedValue = "another value";

Here is a possible implementation of the collection. Note that the Node class is hidden inside, so that we cannot mess around with node from the outside.

public class LinkedList<T> : ICollection<T>
{
    private class Node(T data)
    {
        public Node Next { get; set; }

        public T Data { get; set; } = data;
    }

    private Node _head;
    private Node _tail;

    public T HeadData => _head is null ? default : _head.Data;
    public T TailData => _tail is null ? default : _tail.Data;

    public int Count { get; private set; }

    public bool IsReadOnly => false;

    public void Add(T item)
    {
        var node = new Node(item);
        if (_head is null) {
            _head = node;
            _tail = node;
        } else {
            _tail.Next = node;
            _tail = node;
        }
        Count++;
    }

    public void InsertHead(T item)
    {
        var node = new Node(item);
        if (_head is null) {
            _head = node;
            _tail = node;
        } else {
            node.Next = _head;
            _head = node;
        }
        Count++;
    }

    public void Clear()
    {
        _head = null;
        _tail = null;
        Count = 0;
    }

    public bool Contains(T item)
    {
        var node = _head;
        while (node is not null) {
            if (item is null && node.Data is null || node.Data?.Equals(item) == true) {
                return true;
            }
            node = node.Next;
        }
        return false;
    }

    public void CopyTo(T[] array, int arrayIndex)
    {
        var node = _head;
        while (node != null && arrayIndex < array.Length) {
            array[arrayIndex++] = node.Data;
            node = node.Next;
        }
    }

    public IEnumerator<T> GetEnumerator()
    {
        var node = _head;
        while (node != null) {
            yield return node.Data;
            node = node.Next;
        }
    }

    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();

    public bool Remove(T item)
    {
        Node prev = null;
        var node = _head;
        while (node is not null) {
            if (item is null && node.Data is null || node.Data?.Equals(item) == true) {
                if (prev is null) {
                    _head = node.Next;
                } else {
                    prev.Next = node.Next;
                }
                if (_tail == node) {
                    _tail = prev;
                }
                Count--;
                return true;
            }
            prev = node;
            node = node.Next;
        }
        return false;
    }
}

Usage example:

var list = new LinkedList<Data>();
list.Add(new StringData("Hello"));
list.Add(new IntData(42));
foreach (Data data in list) {
    object before = data.Value;
    data.DoSomethingWithValue();
    object after = data.Value;
    Console.WriteLine($"{before} => {after}");
}

If we add these two implicit operators to the Data class:

public static implicit operator Data(string s) => new StringData(s);
public static implicit operator Data(int i) => new IntData(i);

We can now simply write:

var list = new LinkedList<Data>();
list.Add("Hello");
list.Add(42);

The value "Hello" will automatically be converted to a StringData and the value 42 to an IntData.

To allow the inverse conversion, we can add this line to the TypedData<T> class:

public static implicit operator T(TypedData<T> data) => data.TypedValue;

Then we can write:

TypedData<string> tdata = new StringData("Hello");
string s = tdata;

But this does not work with data typed as Data, because then the type is unknown at compile-time.

Sign up to request clarification or add additional context in comments.

3 Comments

You are right! I don’t really need to know the Node type, but the Data type. I expected that the knowledge of the Node type (and in this way - of the data type) could make it possible in some "magic" way to use automatically the properties or methods of the corresponding type. However, after having read your answer, I realized that there is no Santa, and I have to declare Data as a non-generic object and do:
If(node.Data is string s) DoSomethingWithStrings(s.Substring(0, 3)); Of course this can’t be done with an int, because an int does not implement method Substring(). Thus, I have to go the long way and repeat this check for all expected data types in order to be able to use the particular properties/methods. No way around this. Isn’t it ridiculous that somebody else has to help you to see the most obvious? Thank you, Olivier! P.S. The Primary constructor has no importance in this case. As you wrote, it’s just a syntactic sugar…
I tried to upvote my favorite answers, including yours, but this is obviously not possible without having enough reputation. The linked list cannot have nodes of the same type only, because the measurement data I receive consists of diverse data chunks, e.g. ID, value, unit, measurement metadata etc. which have different types but belong together. Nevertheless, I accepted your answer because it gave me new ideas and valuable concepts for my solution. I appreciate very much your great contribution! Thank you!
0

You can add an Data property also to your base class. This property has an object as type. In your derived generic class, the property still has its generic type.

 public class Node{  
      public Node NextNode{get; set;}
      public object Data {get;set;}
      public static Node<T> WithData<T>(T data) => new Node<T>(data);  
    }  
    public class Node<T>: Node{  
      public T TypedData
      {
         get => (T)Data;
         set => Data = value;
      }  
      public Node(T data) => TypedData = data;  
    }  

This approach has several disadvantages: If you set Data to something not of type T, the getter of TypedData will throw an exception. Moreover, if your node is an instance of your base class, you only can access your data only as an object, which means that you can't to a lot of useful things. But maybe this is already a better approach than your current code.

1 Comment

Cool! Concerning the exception, I am just thinking if try/catch could be used in the TypedData getter and return default(T) if an exception is thrown. Hmmm…, default(T) could be a valid value for T, especially for value types. Depends on the use case... Or doing the Data setter protected? In any case, thanks for the idea! I will definitely test it.
0

I don't think your question has much to do with downcast. Do you realize that this is actually related to how the Notify method is defined.

  • If it is Notify(object), then you can add an object type property to the Node class to return data.

    public abstract class Node
    {  
        public virtual object NodeData => throw new NotImplementedException();
    }  
    
    public class Node<T>: Node
    {  
        public T Data{get; set;}
        public override object NodeData => Data;
    }
    
    Notify(node1.NextNode.NodeData);
    
  • If it is Notify<T>(T), then you can add an method to the Node class, all inherited classes can implement to call the Notify method themselves.

    public abstract class Node
    {  
        public virtual void NotifyNode() {}
    }  
    
    public class Node<T>: Node
    {  
        public T Data{get; set;}
        public override void NotifyNode() => Notify(Data);
    }
    
    node1.NextNode.NotifyNode();
    
  • If Notify is a bad for maintenance, verbose, especially with many data types of overloaded method set (Notify(int), Notify(string) ...), then the solution won't be simple, but you can use switch statement or switch expression and type pattern to improve readability.

    switch(node1.NextNode)
    {
        case Node<int> intNode: Notify(intNode.Data); break;
        case Node<string> stringNode: Notify(stringNode.Data); break;
    };
    

4 Comments

I guess Notify() is something like Console.WriteLine(). OP doesn't call it with a Node<T> as argument, but with boolean and type.
I just want to explain that the answer the OP wants depends on the way of handling that generic type data. It doesn't matter what Notify is, it can be other methods.
@SomeBody: Your assumption is correct. I am using WPF and the MVVM pattern, and Notify(object obj) is just a thin non-generic wrapper around a MessageBox, centered in its parent view. It would have been better if I used in my question Console.WriteLine to avoid misunderstandings.
@shingo: Very valuable ideas and all better than mine! I like especially your generic version of DoDomething<T> as a part of the generic Node<T>. I find generally the switch expression with type pattern very elegant but here it makes the code really very clean and maintainable, no matter how many overloads are needed. Thank you!
0

Based on the OPs comment I would suggest introducing classes for each type data that is needed, and not intermingle different kinds of data in the same list. Something like:

public interface IDeviceData
{
    public string Name { get; }
}

public class TemperatureData : IDeviceData
{
    public string Name { get; init; }
    public List<Temperature> Temperatures { get; } = new();
}

public class GpsData : IDeviceData
{
    public string Name { get; init;}
    public List<string> GpsPositions { get; } = new();
}

This makes it much clearer what type of data you can handle, and minimizes the risk that someone has added some completely unexpected type of data to your list. It also makes it easier to add metadata about where the data originated.

if you need to save the data you can use polymorphic json serialization or use the inheritance support in entify framework.

If you need to handle different kinds of data in different ways you have a few options

  1. Add a method to the interface
  2. Use pattern matching
  3. Use the visitor pattern

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.