Reputation: 23
Put simply, I am hoping to create an interface to be implemented by two classes. The interface and the method within it are tricky to declare, however, due to their use cases.
In essence, these are the two classes that implement this new interface. The interface method Foo()
in each case modifies the referenced object in some manner and outputs a copy of the original object.
public class MyClass1
{
private int _value;
public MyClass1(int val) { _value = val; }
public void Foo(out MyClass1 result)
{
result = new MyClass1(_value);
_value += 1;
}
}
public class MyClass2
{
private double _value;
public MyClass2(double val) { _value = val; }
public void Foo(out MyClass2 result)
{
result = new MyClass2(_value);
_value += 1.5;
}
}
My hope is to find the most intuitive, elegant way to declare and implement interface IInterface
as well as void Foo()
.
Below are some options I have tried as well as the apparent pros and cons of each.
public interface IInterface<T> where T : class
{
void Foo(out T result);
}
The only apparent problem with this solution is that the "generic" type T
in our case is always going to be the class implementing the interface, not an arbitrary type (i.e. IEnumerable<T>
). While this solution works well, it does cause an ugly redundancy in the class implementation:
public class MyClass1 : IInterface<MyClass1> // Ew!
{
// ...
public void Foo(out MyClass1 result) { /* ... */ }
}
Despite the strange class definition, the method's usage is very straightforward:
void Bar(MyClass1 orig)
{
orig.Foo(out MyClass1 copy);
// ...
}
public interface IInterface
{
void Foo<T>(out T result) where T : IInterface; // Gross!
}
This option has far greater issues. The method declaration is more confusing, and additional error handling is required in the implementation:
public class MyClass1 : IInterface
{
// ...
public void Foo<T>(out T result) where T : IInterface // Gross again!
{
// Nothing preventing mismatched types here...
if (typeof(T) != typeof(MyClass1))
throw new Exception("Uh oh!");
// ...
}
}
As mentioned, it is also easy to misuse the method due to the looser type restrictions:
void Bar(MyClass1 orig)
{
// Type mismatch!
orig.Foo<MyClass2>(out MyClass2 copy);
// ...
}
public interface IInterface
{
void Foo(out IInterface result);
}
While this solution eliminates all redundant generics, it sadly leaves room for type mismatch errors in the implementation and usage of the method.
public class MyClass1 : IInterface
{
// ...
public void Foo(out IInterface result)
{
// ...
// Could output MyClass2 instead!
result = new MyClass1(_value);
}
}
The above issue is fairly insignificant, but the below cases are definitely potential issues:
void Bar(MyClass1 orig)
{
orig.Foo(out IInterface copy);
// Can cast copy as wrong type
MyClass2 a = (MyClass2)copy;
// Same problem can happen with 'as' keyword
MyClass2 b = copy as MyClass2;
// ...
}
While I don't think it is currently possible, it would be much cleaner to be able to define the interface method Foo()
with a "pseudo-generic" out T
that is restricted to the implementing class only. Something like this would be ideal:
public interface IInterface
{
// In this context, I would want "this" or some other keyword
// (self, etc.) to indiciate the class implementing the interface
void Foo(out T result) where T : this;
}
public class MyClass1 : IInterface
{
// In an ideal world, this would be valid and correct
void Foo(out MyClass1 result) { /* ... */ }
}
This solution would:
void Bar(MyClass1 orig)
{
// Allow concrete type here rather than interface
orig.Foo(out MyClass1 copy);
// ...
}
Thanks for reading! Would love to hear anyone's thoughts on the best method to use for this kind of case or any potential alternatives to the solutions I've tried.
Upvotes: 2
Views: 100
Reputation: 48139
Instead of going the interface route, might you be able to get away with subclassing instead? Make the top-level of each class derived from a parent. Much simpler declarations. Then the rest of your classes can contain whatever else is specific about themselves that are different. Something like
public class BaseClass<T,T2> where T2 : new()
{
public virtual T2 Foo()
{
var newObj = new T2();
// anything else you want to do with it?
return newObj;
}
protected T _value;
}
public class BaseOne : BaseClass<int, BaseOne>
{
public BaseOne()
{
_value = 1;
}
}
public class BaseTwo : BaseClass<double, BaseTwo>
{
public BaseTwo()
{
_value = 1.5;
}
}
In the declaration, the first "T" is your data type of int or double. The second "T2" requires that allow being created via a "new()" call. So by providing the class you are building as the T2, you get that class type created directly.
Since each individual subclass has its default constructor with no parameters, and the "_value" property as protected (available to be changed at the subclass level of the parent), They can each respectively initiate the value to 1 or 1.5 as needed (or whatever value you actually need).
So the constructor of each type takes care of the startup "_value" respectively with 1 or 1.5. The single FOO call creates and returns the new instance without issue. Then, when trying to create each object respectively, it looks simple like
var x1 = new BaseOne();
var y1 = new BaseTwo();
Because the class declaration of each is already declared as :
BaseClass<T,??> of <int, BaseOne> or <double, BaseTwo>
respectively, you completely mask the type in the new() calling.
Then, to get your new instance of same type
var x2 = x1.Foo();
var y2 = y1.Foo();
Also, instead of doing a NEW() call, you are probably looking to CLONE() if there are a bunch of other properties / values you are trying to carry forward.
Upvotes: 0
Reputation: 36361
There is no generic where T : this
constraint. If you think this is a problem that needs fixing you can submit a request to https://github.com/dotnet/csharplang
The closest alternative would probably be the "curiously reoccurring template pattern": interface IInterface<T> where T : IInterface<T>
, but in this case it does not seem to add anything compared to your first alternative.
I would disagree that the first alternative has a confusing class declaration. While the type is repeated, it is a rather minor issue that only seem to occur in fairly specific circumstances.
Upvotes: 3