DoubleTrouble
DoubleTrouble

Reputation: 902

Merging observable sequences when types derive from the same interface

I have a situation where I want to merge two observable sequences. The sequence consists of Messages like in Ex1 in the code example. I don't understand why it doesn't compile, when Ex2 compiles and runs as expected.

Can anyone explain this to me?

class Message<T> where T : IFoo {}
interface IFoo {}
class Foo : IFoo {}
class Bar : IFoo {}

class Program
{
    static void Ex1()
    {
        var o1 = Observable.Interval(TimeSpan.FromMilliseconds(200)).Select(x => new Message<Foo>());
        var o2 = Observable.Interval(TimeSpan.FromMilliseconds(200)).Select(x => new Message<Bar>());

        o1.Merge<Message<IFoo>>(o2).Subscribe(Console.WriteLine);
        Console.ReadKey();
    }

    static void Ex2()
    {
        var o1 = Observable.Interval(TimeSpan.FromMilliseconds(200)).Select(x => new Foo());
        var o2 = Observable.Interval(TimeSpan.FromMilliseconds(200)).Select(x => new Bar());

        o1.Merge<IFoo>(o2).Subscribe(Console.WriteLine);
        Console.ReadKey();
    }
}

Error in Visual Studio 2017 with .Net Framework 4.6.1:

Error   CS1929  'IObservable<Message<Foo>>' does not contain a definition for 'Merge' and the best extension method overload 'Observable.Merge<Message<IFoo>>(IObservable<IObservable<Message<IFoo>>>, int)' requires a receiver of type 'IObservable<IObservable<Message<IFoo>>>'

Many thanks in advance!

Upvotes: 3

Views: 117

Answers (3)

Sentinel
Sentinel

Reputation: 3697

In addition to other answers here:

interface IMessage<out T> where T : IFoo { }
class Message<T> : IMessage<T> where T:IFoo { }
interface IFoo { }
class Foo : IFoo { }
class Bar : IFoo { }

class Program
{
    static void Ex1()
    {
        var o1 = Observable.Interval(TimeSpan.FromMilliseconds(200)).Select(x => (IMessage<IFoo>)new Message<Foo>());
        var o2 = Observable.Interval(TimeSpan.FromMilliseconds(200)).Select(x => (IMessage<IFoo>)new Message<Bar>());

        o1.Merge(o2).Subscribe(Console.WriteLine);
        Console.ReadKey();
    }

    static void Main(string[] args)
    {
        Ex1();
    }
}

Upvotes: 1

Enigmativity
Enigmativity

Reputation: 117029

Just because Foo is derived from IFoo it does not mean that Message<Foo> derives from Message<IFoo> - it does not - so you can not cast it like you have. The same applies to Bar and IBar.

Let's see why.

Instead of thinking in terms of Message<T>, let's use List<T>. I now have this code:

var foos = new List<Foo>();

foos.Add(new Foo());
foos.Add(new Foo());

That's good.

Now, like your question, when you try to change a Message<Foo> into a Message<IFoo>, let's try changing this to a List<IFoo>:

List<IFoo> ifoos = (List<IFoo>)foos; // Does not compile

Let's say that it did compile - for the sake of explaining why your code didn't work - so I would then have a ifoos that I could do this with:

ifoos.Add(new Bar());

After all, the reference I have for ifoos is of type List<IFoo> and Bar derives from IFoo so that would be perfectly valid code.

But ifoos is just a cast of List<Foo> and casting an object doesn't change its underlying type. A List<Foo> can only accept elements that are of type Foo (or derived from Foo). Now, given our magic change to the compiler, we've created a situation that it can accept Bar.

Boom! Run-time error.

That should clearly be why the cast (List<IFoo>)foos should fail. And that's why the cast (Message<IFoo>)Message<Foo> also fails in your question.

Upvotes: 1

Ciaran_McCarthy
Ciaran_McCarthy

Reputation: 263

We'll start with why the second method works.

The second method creates two collections of type Foo and Bar. But in the Merge method the type to use is explicitly the interface IFoo. This tells the method that this is the type of objects to store in the new collection: IEnumerable<IFoo>. The collection accepts items of type IFoo, and since Foo and Bar both inherit from the interface they're accepted by the collection. If you were to remove the type parameter from the Merge method, or specify explicitly Foo or Bar as the type you would have the same error as the first method.

So what about the first method?

The first collection is of type Message<Foo>. The generic type for this Message is Foo. Note that this is not the same as Message<IFoo>, because the type is explicitly Foo, not its superclasses or interfaces. Similarly Message<Bar> has a generic type of Bar, not IFoo.

When the compiler comes to the Merge method with two collections (one of Message<Foo> and Message<Bar>) it knows that the generic types of the two Messages are not the same (a Foo is an IFoo and an Bar is an IFoo, but a Foo is not a Bar, and vice versa). So the compiler gives an error saying that it expects a collection of a related type (requires a receiver of type 'IObservable<IObservable<Message<IFoo>>>').

There's one final thing to be aware of.

The definition of the Message class is class Message<T> where T : IFoo {}. All the where clause means means is that T must be an IFoo and that's it. It does not mean that a Message<Foo> is a Message<IFoo>. You can cast from a Foo to an IFoo and that's what happens in the second method, but they are not the same.

Upvotes: 1

Related Questions