[UniRx] Give IReadOnlyReactiveCollection covariance

3 minute read

The other day, I was touching Unity and wanted to do something like the following.

interface IItemInfo{ ... }
class ItemInfo : IItemInfo{ ... }

class TestClass {
    private ReactiveCollection<ItemInfo> m_rxCollection = new ReactiveCollection<ItemInfo>();
    public IReadOnlyReactiveCollection<IItemInfo> ObservableCollection => m_rxCollection;
}

I want to publish a ReactiveCollection that stores an instance of a certain class as ReadOnly, but I also want to publish the contents as a __ interface instead of passing the class directly.

But if you do it normally, you will get angry.

image.png

However, for example, if you publish List as IReadOnlyList , you will not get angry.

image.png

This is because the IReadOnlyList interface allows __generics covariance__.

Covariance is explained in detail on other sites, so I will omit it, but the point is that it is a function that allows polymorphism to be applied to __type arguments, and if you add the out modifier to __type arguments, it will be common. Degeneration can be used __.

The out modifier guarantees that the object of the type specified by the generics argument will not be changed, so if it is immutable, the type argument can also be safely type-converted.

The following site is very easy to understand and recommended for detailed explanations.
https://ufcpp.net/study/csharp/sp4_variance.html

So, if you look at the definition of the IReadOnlyList interface, the type argument has the out modifier.
image.png

On the other hand, if you look at the definition of the IReadOnlyReactiveCollection interface, the type argument does not have the out modifier.

image.png

So, if the IReadOnlyReactiveCollection interface also has covariance, it will be solved.

Give IReadOnlyReactiveCollection covariance

Let’s add the out modifier to the type argument of the interface of IReadOnlyReactiveCollection to have covariance. However, if you add the out modifier without thinking about anything, you will get angry.

image.png

The reason is that the members of the IReadOnlyReactiveCollection interface do not guarantee that T is immutable.
For example, CollectionAddEvent has a setter of T, so its value may be rewritten. Therefore, T is not immutable and the error is occurring.

T defines an immutable IReadOnlyCollectionAddEvent interface

So let’s define an interface IReadOnlyCollectionAddEvent for CollectionAddEvent that guarantees that T is immutable.

public interface IReadOnlyCollectionAddEvent<out T>
{
    int Index { get; }
    T Value { get; }
}

Removed T Setter. Originally it was a private set, so there should be no problem.
This ensures that T is invariant for operations performed through the IReadOnlyCollectionAddEvent interface, allowing covariance to be used on a sunny day.

Define the IReadOnlyReactiveCollectionOut interface

Do not rewrite the IReadOnlyReactiveCollection interface directly using the IReadOnlyCollectionAddEvent interface you defined earlier. Changing the members of an interface affects all classes that implement that interface.

Therefore, let’s define a new special IReadOnlyReactiveCollectionOut interface that allows covariance.


public interface IReadOnlyReactiveCollectionOut<out T> : IEnumerable<T>
{
    IObservable<IReadOnlyCollectionAddEvent<T>> ObserveAdd();
    IObservable<IReadOnlyCollectionRemoveEvent<T>> ObserveRemove();
}

This time, I was only planning to use ObserveAdd () and ObserveRemove (), so I defined these two for the time being.
If you want to use other members, add them as appropriate.

Now implement this new IReadOnlyReactiveCollectionOut interface in the ReactiveCollection class. I implemented it as follows. It's just converting the return value of a normal ObserveAdd () to an interface.


IObservable<IReadOnlyCollectionAddEvent<T>> IReadOnlyReactiveCollectionOut<T>.ObserveAdd()
    => ObserveAdd().Select(d => (IReadOnlyCollectionAddEvent<T>)d);
IObservable<IReadOnlyCollectionRemoveEvent<T>> IReadOnlyReactiveCollectionOut<T>.ObserveRemove()
    => ObserveRemove().Select(d => (IReadOnlyCollectionRemoveEvent<T>)d);

It’s been a little long, but now you can “publish the ReactiveCollection ReadOnly and the contents in the interface instead of directly", which was the original purpose!

Try using

The code in question

I want to make ReactiveCollection ReadOnly and the contents are also open to the interface, but I can't.

image.png

This is because the IReadOnlyReactiveCollection interface does not guarantee T immutability.

Improved code

On the other hand, if you publish with the countermeasure IReadOnlyReactiveCollectionOut interface,

image.png

No more errors!
That’s because the IReadOnlyReactiveCollectionOut interface guarantees T immutability.

Finally

I think that there are many cases where the ReactiveCollection is published as ReadOnly, but this time I wanted to publish the contents class as an interface, so I tried to take measures.
I hope it is supported by the UniRx standard …

There is no problem so far, but I will write an article if any inconvenience arises in the future.