I'm having a hard time describing to problem in the title, sorry if the title isn't the clearest.
Suppose I have the following interface/classes :
public interface IAction { /*...*/ }
public class WriteAction : IAction { /*...*/ }
public class ReadAction : IAction { /*...*/ }
Now, these actions will be used in other classes. ReadWriteTest
can have both ReadAction
and WriteAction
objects, but ReadTest
can only have ReadAction
objects.
Note that these test objects do more than just this, I'm redacting other functionalities as they are not pertinent to the question.
Both xxxTest
classes share a common interface. So the implementation of the interface and ReadWriteTest
class is as follows:
public interface ITest
{
List<IAction> Actions { get; set; }
}
public class ReadWriteTest : ITest
{
public List<IAction> Actions { get; set; }
}
The problem is that for the ReadTest
class, I would like to limit this property to only contain ReadAction
elements.
What I tried to implement is the following:
public class ReadTest : ITest
{
public List<ReadAction> Actions { get; set; }
}
It seems to me that this should work, as every ReadAction
inherently implements the IAction
interface.
However, the compiler doesn't like it, and tells me the ReadTest
class doesn't implement all needed IAction
properties.
Is there a way to restrict the content of this Actionlist in the ReadTest
class definition?
I can work around it by creating a custom AddAction(IAction action)
method that simply does not add any WriteAction
objects, but I was hoping for a more elegant solution to this problem.
Is this possible?
Update on the final result
As answered, it can be done by adding a generic type to the interface. This solves the problem I'm describing, but sadly does not solve the larger problem. Adding this generict type means that I now can't address either Test object as an ITest
, as I'm now required to specify the generic parameter, which is different for either Test.
The solution (or at least, my solution) is to remove the Actions
property from the ITest
interface. The Actions
properties still exist in the classes.
Instead of having the list property defined in the interface, I added a Run()
method to the interface. This method will iterate over the locally defined Actions
list in either Test class.
So as a quick overview:
foreach(ITest myTest in myTests)
{
myTest.Run()
}
Class implementation for ReadWriteTest
:
public List<IAction> Actions {get; set;}
public void Run()
{
foreach(IAction action in Actions) { /*...*/ }
}
Class implementation for ReadTest
:
public List<ReadAction> Actions {get; set;}
public void Run()
{
foreach(IAction action in Actions) //I could also declare it as a ReadAction. Either works.
{ /*...*/ }
}
Use the Omit utility type to override the type of an interface property, e.g. interface SpecificLocation extends Omit<Location, 'address'> {address: newType} . The Omit utility type constructs a new type by removing the specified keys from the existing type.
An interface can't be instantiated directly. Its members are implemented by any class or struct that implements the interface. A class or struct can implement multiple interfaces. A class can inherit a base class and also implement one or more interfaces.
// within the class. Interface inheritance : An Interface can extend other interface.
Although you cannot override the method in C#, what an XML comment does is simply produce an entry in the accompanying XML file. Tools that interpret these comments may or may not do the right thing if you manually insert the documentation for the "new" methods.
You could fix this by using a generic type in your ITest
interface:
public interface ITest<T> where T : IAction
{
List<T> Actions { get; set; }
}
Note the type constraint which forces the passed in type to be IAction
. This in turn makes your subclasses this:
public class ReadWriteTest : ITest<IAction>
{
public List<IAction> Actions { get; set; }
}
public class ReadTest : ITest<ReadAction>
{
public List<ReadAction> Actions { get; set; }
}
If you can accept changing the interface so that you can't set the actions directly via the property, then you can make the class covariant by using the out
keyword.
This will then allow you to create, for example, an object of type ReadWriteTest
and pass it to a method which accepts a parameter of type ITest<IAction>
.
Here's a complete compilable console app to demonstrate:
using System;
using System.Collections.Generic;
namespace Demo
{
// The basic IAction interface.
public interface IAction
{
void Execute();
}
// Some sample implementations of IAction.
public sealed class ReadAction: IAction
{
public void Execute()
{
Console.WriteLine("ReadAction");
}
}
public sealed class ReadWriteAction: IAction
{
public void Execute()
{
Console.WriteLine("ReadWriteAction");
}
}
public sealed class GenericAction: IAction
{
public void Execute()
{
Console.WriteLine("GenericAction");
}
}
// The base ITest interface. 'out T' makes it covariant on T.
public interface ITest<out T> where T: IAction
{
IEnumerable<T> Actions
{
get;
}
}
// A ReadWriteTest class.
public sealed class ReadWriteTest: ITest<ReadWriteAction>
{
public ReadWriteTest(IEnumerable<ReadWriteAction> actions)
{
_actions = actions;
}
public IEnumerable<ReadWriteAction> Actions
{
get
{
return _actions;
}
}
private readonly IEnumerable<ReadWriteAction> _actions;
}
// A ReadTest class.
public sealed class ReadTest: ITest<ReadAction>
{
public ReadTest(IEnumerable<ReadAction> actions)
{
_actions = actions;
}
public IEnumerable<ReadAction> Actions
{
get
{
return _actions;
}
}
private readonly IEnumerable<ReadAction> _actions;
}
// A GenericTest class.
public sealed class GenericTest: ITest<IAction>
{
public GenericTest(IEnumerable<IAction> actions)
{
_actions = actions;
}
public IEnumerable<IAction> Actions
{
get
{
return _actions;
}
}
private readonly IEnumerable<IAction> _actions;
}
internal class Program
{
private static void Main()
{
// This demonstrates that we can pass the various concrete classes which have
// different IAction types to a single test method which has a parameter of
// type ITest<IAction>.
var readActions = new[]
{
new ReadAction(),
new ReadAction()
};
var test1 = new ReadTest(readActions);
test(test1);
var readWriteActions = new[]
{
new ReadWriteAction(),
new ReadWriteAction(),
new ReadWriteAction()
};
var test2 = new ReadWriteTest(readWriteActions);
test(test2);
var genericActions = new[]
{
new GenericAction(),
new GenericAction(),
new GenericAction(),
new GenericAction()
};
var test3 = new GenericTest(genericActions);
test(test3);
}
// A generic test method.
private static void test(ITest<IAction> data)
{
foreach (var action in data.Actions)
{
action.Execute();
}
}
}
}
If you want to be able to set the actions after creating the objects, you can add to each concrete class a setter method.
For example, you could change the ReadWriteTest
class to:
public sealed class ReadWriteTest: ITest<ReadWriteAction>
{
public void SetActions(IEnumerable<ReadWriteAction> actions)
{
_actions = actions;
}
public IEnumerable<ReadWriteAction> Actions
{
get
{
return _actions;
}
}
private IEnumerable<ReadWriteAction> _actions = Enumerable.Empty<ReadWriteAction>();
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With