user3705416
user3705416

Reputation: 115

Is there a way to pass arrays of different types to an interface implementation

So I have the following scenario:

I'm writing a program which accepts content in arrays of various types (string, byte, etc.), parses that content into a data structure, and then stores the structured data somewhere. Since the parsing methods can be pretty different, depending upon which type of data is received, I wanted to define separate file parsing classes, but define methods and properties that they should all implement. I wanted to use an interface for this, but I can't figure out how to tell the interface that its ParseFile method, for example, is going to accept some type of array as an input parameter. If I write something like the following in an interface:

void ParseFile(Array[] fileContents);

And then try to implement that method in a ByteParser class, like so:

public void ParseFile(Byte[] fileContents){
//Do whatever
}

I get a compiler error, that my implementing class doesn't implement the ParseFile method. I'm thinking that's because the compiler doesn't cast by default, and even though ByteArray is a derivative of Array, it's not of type Array.

Is there any way to do what I'm trying to do here, force the interface to accept any type of array and allow my implementing classes to handle it from there?

Upvotes: 0

Views: 428

Answers (1)

Dai
Dai

Reputation: 155698

Your current idea (of using Array (not Array[]) and Byte[]) is... ill-informed and ill-advised.

For one, .NET requires implementations of an interface to exactly match the interface definition, so if you have a method void ParseFile(Array fileContents) then the implementation must also have void ParseFile(Array fileContents) - you cannot have ParseFile(Byte[] fileContents). Here's why you can't:

Supposing that this actually compiles:

interface IFileBuilder {
    void ParseFile(Array fileContents);
}

class OctetFileBuilder : IFileBuilder {
    public void ParseFile(Byte[] fileContents) {
        // ...
    }
}

...then run this:

void Main() {
    
    Byte[]   byteArray = new Byte[] { 0x01, 0x02, 0x03, 0x04, 0x05 };
    String[] strArray  = new String[] { "foo", "bar", "baz" };

    OctetFileBuilder ofb = new OctetFileBuilder();
    ofb.ParseFile( byteArray ); // OK, so far so good.

    ofb.ParseFile( strArray ); // Compiler rejects this because `String[]` isn't `Byte[]`. That's also OK.

    IFileBuilder fb = ofb;
    fb.ParseFile( byteArray ); // This would be okay because a `Byte[]` can be the argument for an `Array` parameter.

    fb.ParseFile( strArray ); // But what happens here?
    
}

That last line there is the problem: fb.ParseFile( strArray ).

The implementation expects Byte[] but it's being passed a String[].

I imagine you would expect .NET to throw a runtime error - or that it would somehow magically know how to convert String[] to Byte[] (no, it won't). Instead that entire program simply won't compile.

Now, in reality, your implementation would have to look like this in order for it to build:

class OctetFileBuilder : IFileBuilder {
    public void ParseFile(Array fileContents) {
        Byte[] byteArray = (Byte[])fileContents; // Runtime cast.
    }
}

...so now your code will compile and start to run, but it will crash before it can complete because when ParseFile is given a String[] it will cause an InvalidCastException because you can't meaningfully directly cast from String[] to Byte[].

Solution: Covariant generics!

This is what generic types (and contravariance and covariance) are all about and why when designing polymorphic interfaces you need to think carefully about the direction of the movement of data inside your program (as generally speaking, "forward"/"input" data can be "shrunk" and "output" data can be "grown"/"expanded"/"extended" - but not vice-versa. This is what the in and out generic-type modifiers are for.

So I'd do it like this: (disclaimer: I have no idea what your interface is actually for, nor why you would want anything other than Byte[] for your ParseFile method parameter...)

interface IFileBuilder<in TInput>
{
    void ParseFile( IReadOnlyList<TInput> fileContents )
}

class OctetFileBuilder : IFileBuilder<Byte> {
    public void ParseFile(Byte[] fileContents) {
        // ...
    }
}

(Note that in this particular case the use of contravariance/covariance modifiers (the in TInput part) is pointless because the primitive and base-types in .NET (Byte, Int32, String, etc) are either structs without inheritance or are sealed-types).

Usage:

void Main() {
    
    Byte[]   byteArray = new Byte[] { 0x01, 0x02, 0x03, 0x04, 0x05 };
    String[] strArray  = new String[] { "foo", "bar", "baz" };

    OctetFileBuilder ofb = new OctetFileBuilder();
    ofb.ParseFile( byteArray ); // OK, so far so good.

    ofb.ParseFile( strArray ); // Compiler rejects this because `String[]` isn't `Byte[]`. That's also OK.

    IFileBuilder<Byte> fb = ofb;
    fb.ParseFile( byteArray ); // OK

    fb.ParseFile( strArray ); // Compiler error: `String[]` is not `IReadOnlyList<Byte>`.
    
}

So your hypothetical runtime error is now a guaranteed compile-time error.

And compile-time errors are much better than runtime errors - and this approach also means you can reason about the direction of data-flow in your application.

Upvotes: 1

Related Questions