Search code examples
c#typescastingnumsharp

C# NumSharp undefined behavior when using `object` parameter compared to explicit type parameter


I am working with NumSharp and a bit new to it. I would like to set multiple data values and there are two main functions of concern:

NDArray.SetValue(object value, param int[] indices)
NDArray.SetData(object value, param int[] indices)

The first one SetValue expects full index of a single value element, for example, if you have np.zeros(3,3) it expects full indexing, and if there is missing index it will use 0 in its place.

For example:

var arr = np.zeros(3,3);
arr.SetValue(10.0, 0); 
arr.SetValue(10.0, 0, 0); 

Both lines are equivalent.

On the other hand, SetData will modify an entire sub-array based on the index, and if you give full index, it works like SetValue. For example:

arr.SetData(10.0, 0); // sets all arr[0] values to 10
arr.SetData(10.0, 0, 1); // sets arr[0][1] to 10

Now, for my use case, I would like to redefine a function that accepts multiple values and treats each one as a sub-array index.

arr.SetMultiData(10.0, 0, 1); /// sets arr[0] and arr[10] values to 10

Naturally, I went for an extension method:

public static class Extensions
{
    public static void SetMultiData(this NDArray arr, object value, int[] indices)
    {
        foreach (var i in indices)
            arr.SetData(value, i);
    }
}

But here is what completely lost me. This code, which does not use extension method, works:

    static void Main(string[] args)
    {
        NDArray arr = np.zeros(3, 3);
        int[] indices = new int[] { 0, 1 };
        double value = 10;

        foreach (var i in indices)
            arr.SetData(value, i); 

        Console.WriteLine(arr.ToString());
    }

Output:

[[10, 10, 10],[10, 10, 10],[0, 0, 0]]

This code doesn't:

    static void Main(string[] args)
    {
        NDArray arr = np.zeros(3, 3);
        int[] indices = new int[] { 0, 1 };
        double value = 10;

        arr.SetMultiData(value, indices);

        Console.WriteLine(arr.ToString());
    }

Output:

[[10, 0, 0],[0, 0, 0],[0, 0, 0]]

This isn't always going to print, in most cases it just fails to print and I believe the underlying arr structure is corrupted accordingly, which leads to undefined behavior when I run it multiple times.

Now, the last bit. When I change the Extension method to accept double instead of object, it works perfectly. But I need the extension method to work with more than one type given it is just a wrapper around SetData. Given this, I also tried changing the method to be generic:

public static class Extensions
{
    public static void SetMultiData<T>(this NDArray arr, object value, int[] indices)
    {
        T val = (T)value;
        foreach (var i in indices)
            arr.SetData(val, i);
    }
}

I was shocked when this didn't work and produced the same output. Basically, I made the explicit cast from object to type. And I called the function like this: arr.SetMultiData<double>(value, indicies); and I used the debugger and compared the code that worked with this and the types are exactly the same ones that are passed to arr.SetValue given i = int and val = double, yet one works and one doesn't.

What causes such behavior? And why does it work when I explicitly set it to double when it fails both using generic/object types?

For reference, here is the SetData implementation. which is open-source.


Solution

  • The reason is double is implicitly convertible to NDArray (by implicit operator defined in NDArray). So when you do:

    arr.SetData(value, i);
    

    and value is of type double - the overload being called is SetData(NDArray, int[), because it's the best fit given that double is convertible to NDArray.

    However when value is of type object or even of unresolved generic type (T) - then overload being called is SetData(object, int[]), so things go wild (because those overloads do quite different things IF value you pass as object is not NDArray, and it's not - it is double). Overload is chosen at compile time, and there is no way compiler can choose the NDArray overload even in generic case (since overload chosen should fit for any possible generic types you can pass).

    In your case you can "solve" this by using NDArray value in your extension method instead of object or generic:

    public static class Extensions
    {
        public static void SetMultiData(this NDArray arr, NDArray value, int[] indices)
        {
            foreach (var i in indices)
                arr.SetData(value, i);
        }
    }