开发者

C# Interface with method that returns an implementation of multiple interfaces

开发者 https://www.devze.com 2023-02-04 19:49 出处:网络
When working with interfaces, I frequently run into the case where I want to ensure that a return value from a property or method, or sometimes a parameter to a method, implements TWO or more interfac

When working with interfaces, I frequently run into the case where I want to ensure that a return value from a property or method, or sometimes a parameter to a method, implements TWO or more interfaces without creating a new interface.

My specific instance right now is that I want to specify that a method will result in a IEnumerable<SomeType> that also supports INotifyCollectionChanged - that way another object using the interface does not have to typecast and can still access both settings. (I don't want to use ReadOnlyObservableCollection explicitly because it only works well with ObservableCollection objects, but I would also like to leave the option open for future implementers of the interface to use it if they want to.)

I'm thinking that this can only be handled with parameters to a method, not return values, by providing a method declaration as follows:

void SetStringData<T>(T data) where T : IEnumerable<string>, INotifyCollectionChanged

Just to be clear, what I'd really like is something where the using class CAN'T specify the exact return type. Similar to the following, but obviously the syntax doesn't work or even make sense.

(IEnumerable<string>, INotifyCollectionChanged) GetStringData()

Any suggestions on how I can do this? Or, fail开发者_开发技巧ing that, how I can achieve the same results?


The only way I can see this being done is making use of of dynamic as its all resolved at runtime, e.g.:

public dynamic GetStringData()
{

}

And:

IDisposable disposable = GetStringData();
ISomeOtherInterface other = GetStringData();

BUT you lose all type safety that the compiler would fall over on. I think the best way to do this is to make a composite interface.

public IComposite GetStringData()
{

}

And:

IEnumerable<string> enumerable = GetStringData();


No good solution, but my best guess is adding a code contract to the interface. Still requires that the caller casts the result the the interface he needs.

Something like:

Contract.Ensures(Contract.Result<object>() is IEnumerable<string>);
Contract.Ensures(Contract.Result<object>() is INotifyCollectionChanged);


You can create another abstraction (an adapter) that caches the return value and provides separate accessors for the different types that you need. This way, client code is shielded from testing and casting. You can return this adapter instead of the original return value.

Or, you can just return a tuple with your desired outputs.


Option 1: Strongly-typed out parameters (aka TryGetValue)

This allows the consumer to select the specific interface they want - even if it does return the same value.

class SettingsStore
{
    public Boolean TryGetStringData( out IEnumerable<String> dataEnumerable ); 
    public Boolean TryGetStringData( out INotifyCollectionChanged dataCollection );
}

Or:

class SettingsStore
{
    public void GetStringData( out IEnumerable<String> dataEnumerable ); 
    public void GetStringData( out INotifyCollectionChanged dataCollection );
}

(My overloads use different out parameter names to allow consumers to select overloads with explicit parameter name labels instead of type-inference which can be painful).

Option 2: Implicit type conversion

This is inspired by OneOf<T...> ( https://github.com/mcintyre321/OneOf ).

Add this type to your project:

static class ImplOf
{
    public static ImplOf<TImpl,T1,T2>( TImpl implementation )
        where TImpl : T1, T2
    {
        return new ImplOf<T1,T2>( implementation );
    }

    public static ImplOf<TImpl,T1,T2,T3>( TImpl implementation )
        where TImpl : T1, T2, T3
    {
        return new ImplOf<T1,T2,T3>( implementation );
    }

    // etc for 4, 5, 6+ interfaces.
}

struct ImplOf<T1,T2>
{
    private readonly Object impl;
    public ImplOf( Object impl ) { this.impl = impl; }

    public static implicit operator T1(ImplOf<T1,T2> self) => (T1)self.impl;
    public static implicit operator T2(ImplOf<T1,T2> self) => (T2)self.impl;

    public static implicit operator ImplOf<T1,T2>(T1 impl) => new ImplOf<T1,T2>( impl );
    public static implicit operator ImplOf<T1,T2>(T2 impl) => new ImplOf<T1,T2>( impl );

    // These properties are for convenience and are not required.
    public T1 Interface1 => (T1)this.impl;
    public T2 Interface2 => (T2)this.impl;
}

struct ImplOf<T1,T2,T3>
{
    private readonly Object impl;
    public ImplOf( Object impl ) { this.impl = impl; }

    public static implicit operator T1(ImplOf<T1,T2,T3> self) => (T1)self.impl;
    public static implicit operator T2(ImplOf<T1,T2,T3> self) => (T2)self.impl;
    public static implicit operator T3(ImplOf<T1,T2,T4> self) => (T3)self.impl;

    public static implicit operator ImplOf<T1,T2,T3>(T1 impl) => new ImplOf<T1,T2,T3>( impl );
    public static implicit operator ImplOf<T1,T2,T3>(T2 impl) => new ImplOf<T1,T2,T3>( impl );
    public static implicit operator ImplOf<T1,T2,T3>(T3 impl) => new ImplOf<T1,T2,T3>( impl );

    public T1 Interface1 => (T1)this.impl;
    public T2 Interface2 => (T2)this.impl;
    public T3 Interface2 => (T3)this.impl;
}

// etc for 4, 5, 6+ interfaces

So your SettingsStore is now:

public class SettingsStore
{
    public ImplOf<IEnumerable<String>,INotifyPropertyChanged> GetStringData()
    {
        MyStringDataCollection collection = ... // `MyStringDataCollection` implements both `IEnumerable<String>` and `INotifyPropertyChanged`.

        return ImplOf.Create<MyStringDataCollection,IEnumerable<String>,INotifyPropertyChanged>( collection );
    }
}

Because of how implicit works, consumers of GetStringData can use it like so:

IEnumerable<String> strings = store.GetStringData();

INotifyCollectionChanged collection = store.GetStringData();

// Or they can use ImplOf directly, but need to use the `InterfaceN` properties:
var collection = store.GetStringData();
foreach( String item in collection.Interface1 ) { }

Option 3: Just define a new interface

I frequently run into the case where I want to ensure that a return value from a property or method, or sometimes a parameter to a method, implements TWO or more interfaces without creating a new interface.

I don't know why you're opposed to defining a new interface type because interface inheritance is the idiomatic C# way of supporting this scenario (because C# does not yet support algebraic types like TypeScript does):

interface ISettingsStrings : IEnumerable<String>, INotifyCollectionChanged
{
    // No interface body is required.
}

If you're concerned about this breaking ABI compatibility if your interface is in-the-wild and you want to add additional interfaces (e.g. IReadOnlyList<String>) then you can just do this:

interface ISettingsStrings : IEnumerable<String>, INotifyCollectionChanged
{
    // No interface body is required.
}

// Declare a successor interface:
interface ISettingsStrings2 : ISettingsStrings, IReadOnlyList<String>
{
}

class SettingsStore
{
    public ISettingsStrings2 GetStringData();
}

ABI consumers of the older SettingsStore.GetStringData() (which had a declared return type of ISettingsStrings) will still work because ISettingsStrings2 implements ISettingsStrings.


Perhaps a generic method would do?

T GetStringData<T>()

And add some restrictions to T


What do you know about the concrete type of the objects the method will be returning? If you know that the return from the interface will be a Widget, and that Widget supports IEnumerable and INotifyCollectionChanged, you could define the function to return Widget. If you know that the return type will be a class that you'll be designing for your indicated purpose, but you don't know exactly what class it will be, you could define a new interface INotifiableEnumerable which derives from both IEnumerable and INotifyCollectionChanged, and have any class you'll be returning implement INotifiableEnumerable. Note that in the latter case it will not be possible for your function to return classes that don't explicitly implement INotifiableEnumerable even if they happen to implement both IEnumerable and INotifyCollectionChanged.

0

精彩评论

暂无评论...
验证码 换一张
取 消