Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to override an interface property with an inherited type

Tags:

c#

interface

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.
    { /*...*/ }
}
like image 560
Flater Avatar asked Sep 26 '14 10:09

Flater


People also ask

How do you override an interface property?

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.

Can inherited class implement interface?

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.

Can an interface inherit an interface?

// within the class. Interface inheritance : An Interface can extend other interface.

Can interface override parent interface method?

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.


2 Answers

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; }
}
like image 122
DavidG Avatar answered Sep 28 '22 17:09

DavidG


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>();
}
like image 43
Matthew Watson Avatar answered Sep 28 '22 17:09

Matthew Watson