Общая идея проста: вы объединяете P и Q, используете BufferWithCount (2), чтобы получить пары значений, а затем обрабатываете пары в соответствии с вашей логикой:
P.Merge(Q).BufferWithCount(2).Select(values =>
{
var first = values[0];
var second = values[1];
if (first is P && second is P ||
first is Q && second is Q)
{
return 0;
}
if (first is P)
{
return selector(first, second);
}
else // suppose Q, P is a valid sequence as well.
{
return selector(second, first);
}
});
Теперь сложная часть состоит в объединенииP и Q, если они разного типа, а затем различают их в Select.Если они относятся к одному и тому же типу, вы можете использовать что-то простое, например, подход, предложенный Enigmativity, то есть
var pqs =
(from p in ps select new { k = "p", v = p })
.Merge(from q in qs select new { k = "q", v = q });
Теперь сложная часть, если они разных типов, для объединения их нам понадобится некоторая общая оболочкатипа, что-то вроде, например, Data.Either от Haskell:
public abstract class Either<TLeft, TRight>
{
private Either()
{
}
public static Either<TLeft, TRight> Create(TLeft value)
{
return new Left(value);
}
public static Either<TLeft, TRight> Create(TRight value)
{
return new Right(value);
}
public abstract TResult Match<TResult>(
Func<TLeft, TResult> onLeft,
Func<TRight, TResult> onRight);
public sealed class Left : Either<TLeft, TRight>
{
public Left(TLeft value)
{
this.Value = value;
}
public TLeft Value
{
get;
private set;
}
public override TResult Match<TResult>(
Func<TLeft, TResult> onLeft,
Func<TRight, TResult> onRight)
{
return onLeft(this.Value);
}
}
public sealed class Right : Either<TLeft, TRight>
{
public Right(TRight value)
{
this.Value = value;
}
public TRight Value
{
get;
private set;
}
public override TResult Match<TResult>(
Func<TLeft, TResult> onLeft,
Func<TRight, TResult> onRight)
{
return onRight(this.Value);
}
}
}
Достаточно забавно, в System.Reactive.dll уже есть похожий класс Either, к сожалению, он внутренний, поэтому нам нужнонаша собственная реализация.Теперь мы можем поместить как P, так и Q в Either и продолжить без решения (я немного обобщил его, поэтому вы можете вернуть любой результат вместо int):
public static IObservable<TResult> SmartZip<TLeft, TRight, TResult>(
IObservable<TLeft> leftSource,
IObservable<TRight> rightSource,
Func<TLeft, TRight, TResult> selector)
{
return Observable
.Merge(
leftSource.Select(Either<TLeft, TRight>.Create),
rightSource.Select(Either<TLeft, TRight>.Create))
.BufferWithCount(2)
.Select(values =>
{
// this case was not covered in your question,
// but I've added it for the sake of completeness.
if (values.Count < 2)
{
return default(TResult);
}
var first = values[0];
var second = values[1];
// pattern-matching in C# is really ugly.
return first.Match(
left => second.Match(
_ => default(TResult),
right => selector(left, right)),
right => second.Match(
left => selector(left, right),
_ => default(TResult)));
});
}
А вот небольшойдемо для всего этого страшного уродливого материала.
private static void Main(string[] args)
{
var psource = Observable
.Generate(1, i => i < 100, i => i, i => i + 1)
.Zip(Observable.Interval(TimeSpan.FromMilliseconds(10.0)), (i, _) => i);
var qsource = Observable
.Generate(1, i => i < 100, i => (double)i * i, i => i + 1)
.Zip(Observable.Interval(TimeSpan.FromMilliseconds(30.0)), (i, _) => i);
var result = SmartZip(
psource,
qsource,
(p, q) => q / p).ToEnumerable();
foreach (var item in result)
{
Console.WriteLine(item);
}
}