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
.