Следующее немного длинно. Если вы заинтересованы только в том, чтобы заставить это работать, и не заботитесь о том, почему или как, то перейдите к последним двум разделам кода.
Томас Петричек ответ был верным направлением, но только на полпути туда. TResult
из resultSelector
действительно должно быть общим. Соединения действительно связаны друг с другом, а промежуточные результаты содержат анонимный тип, состоящий из левой и правой частей каждого объединения (они называются внешними и внутренними, соответственно).
Давайте посмотрим на мой запрос ранее:
TableView result =
from t1 in table1
join t2 in table2 on t1["ID"] equals t2["ID"]
join t3 in table3 on t1["ID"] equals t3["ID"]
select new[]{t1["ID"], t2["Description"], t3["Foo"]};
Это переводится примерно так:
var intermediate =
table1.Join(
table2, t1=>t1["ID"], t2=>t2["ID"],
(t1, t2)=>new{t1=t1, t2=t2}
);
TableView result =
intermediate.Join(
table3, anon=>anon.t1["ID"], t3=>t3["ID"],
(anon, t3)=>new[]{anon.t1["ID"], anon.t2["ID"], t3["Foo"]}
);
Добавление дополнительного соединения делает шаблон более четким:
TableView result =
from t1 in table1
join t2 in table2 on t1["ID"] equals t2["ID"]
join t3 in table3 on t1["ID"] equals t3["ID"]
join t4 in table4 on t1["ID"] equals t4["ID"]
select new[]{t1["ID"], t2["Description"], t3["Foo"], t4["Bar"]};
Это примерно переводит на:
var intermediate1 =
table1.Join(
table2, t1=>t1["ID"], t2=>t2["ID"],
(t1, t2)=>new{t1=t1, t2=t2}
);
var intermediate2 =
intermediate1.Join(
table3, anon1=>anon1.t1["ID"], t3=>t3["ID"],
(anon1, t3)=>new{anon1=anon1, t3=t3}
);
TableView result =
intermediate2.Join(
table4, anon2=>anon2.anon1.t1["ID"], t4=>t4["ID"],
(anon2, t3)=>new[]{
anon2.anon1.t1["ID"], anon2.anon1.t2["ID"],
anon2.t3["Foo"], t4["Bar"]
}
);
Таким образом, возвращаемое значение resultSelector
будет двумя разными вещами. Для окончательного соединения результатом является список выбора, и это тот случай, который я уже обрабатывал. Для каждого другого соединения это будет анонимный тип, содержащий объединенные таблицы, которым присваиваются имена в соответствии с псевдонимами, которые я назначил им в запросе. LINQ, по-видимому, заботится о косвенности в анонимном типе, шагая по мере необходимости.
Стало ясно, что мне нужен не один, а два Join
метода. Тот, который у меня работал просто отлично для финального соединения, и мне нужно было добавить еще один для промежуточных соединений. Помните, что метод, который у меня уже был, возвращает TableView
:
public TableView Join(Table inner,
FakeFunc<Table, Projection> outerKeySelector,
FakeFunc<Table, Projection> innerKeySelector,
FakeFunc<Table, Table, Projection[]> resultSelector){
Table join = new JoinedTable(this, inner,
new EqualsCondition(outerKeySelector(this), innerKeySelector(inner)));
return join.Select(resultSelector(this, inner));
}
Теперь мне нужно было добавить тот, который возвращал что-нибудь с помощью метода Join
, чтобы его можно было вызывать в цепочке:
public Table Join<T>(Table inner,
FakeFunc<Table, Projection> otherKeySelector,
FakeFunc<Table, Projection> innerKeySelector,
FakeFunc<Table, Table, T> resultSelector){
Table join = new JoinedTable(this, inner,
new EqualsCondition(outerKeySelector(this), innerKeySelector(inner)));
// calling resultSelector(this, inner) would give me the anonymous type,
// but what would I do with it?
return join;
}
Добавление этого метода сделало мои объединения почти работающими. Наконец-то я могу объединить три или более таблиц, но потерял псевдонимы:
TableView result =
from t1 in table1
join t2 in table2 on t1["ID"] equals t2["ID"]
join t3 in table3 on t1["ID"] equals t3["ID"]
// ^ error, 't1' isn't a member of 'Table'
select new[]{t1["ID"], t2["Description"], t3["Foo"]};
// ^ ^ error, 't1' & 't2' aren't members of 'Table'
Я почти остановился здесь. В конце концов, я мог бы обойти это, исключив эти потерянные псевдонимы:
TableView result =
from t1 in table1
join t2 in table2 on t1["ID"] equals t2["ID"]
join t3 in table3 on table1["ID"] equals t3["ID"]
select new[]{table1["ID"], table2["Description"], t3["Foo"]};
Это скомпилировало, запустило и дало ожидаемый результат. Woo-Hoo! Успех, вроде. Потеря псевдонимов была не идеальной, хотя. В реальном запросе таблицы могут быть более сложными:
TableView result =
from t1 in table1
join t2 in (
from t in table2
where t["Amount"] > 20
select new[]{t["ID"], t["Description"]
).AsSubQuery() on t1["ID"] equals t2["ID"]
join t3 in table3 on t1["ID"] equals t3["ID"]
select new[]{table1["ID"], t2["Description"], t3["Foo"]};
// ^ error, 't2' isn't a member of 'Table'
Здесь я не мог бы обойтись без псевдонима для t2
(ну, я мог бы, но это включало бы перемещение подзапроса в другую переменную, объявленную до основного запроса, но я пытаюсь бегло говорить здесь).
После того, как я достаточно раз увидел сообщение "t1" не является членом таблицы ", я наконец понял, что секрет был в параметре outerKeySelector
, равном Join
. LINQ просто искал свойство с именем t1
(или что-то еще), которое было бы частью аргумента этой лямбды. Мои outerKeySelector
параметры были объявлены так:
FakeFunc<Table, Projection> outerKeySelector
Класс Table
определенно не имеет свойства с именем t1
или какого-либо другого псевдонима. Как я могу добавить это свойство? Если бы я использовал C # 4, я мог бы, возможно, использовать dynamic
, чтобы сделать это, но если бы я использовал C # 4, тогда весь дизайн был бы другим (и я планирую повторить это позже в C # 4, для. Только для клиентов NET 4.0, использующих все преимущества динамической типизации для предоставления проекций столбцов в качестве фактических свойств таблиц). Однако в .NET 2.0 у меня нет динамических типов. Так, как я мог сделать тип, который имел псевдонимы таблицы в качестве свойств?
Какая минутка. Держи телефон. resultSelector
уже возвращает мне один! Каким-то образом мне нужно держаться за этот объект и передать его outerKeySelector
в следующем соединении. Но как? Я не могу просто сохранить его в своем классе JoinedTable
, потому что этот класс не будет знать, как его разыграть.
Вот когда он ударил меня. Мне нужен общий промежуточный класс для хранения этих промежуточных результатов соединения. Он будет хранить ссылку на экземпляр JoinedTable
, описывающую фактическое соединение, а также ссылку на этот анонимный тип, содержащий псевдонимы. Эврика!
Окончательная, полностью функциональная версия кода добавляет класс:
class IntermediateJoin<T>{
readonly JoinedTable table;
readonly T aliases;
public IntermediateJoin(JoinedTable table, T aliases){
this.table = table;
this.aliases = aliases;
}
public TableView Join(Table inner,
FakeFunc<T, Projection> outerKeySelector,
FakeFunc<Table, Projection> innerKeySelector,
FakeFunc<T, Table, Projection[]> resultSelector){
var join = new JoinedTable(table, inner,
new EqualsCondition(outerKeySelector(aliases), innerKeySelector(inner)));
return join.Select(resultSelector(aliases, inner));
}
public IntermediateJoin<U> Join<U>(Table inner,
FakeFunc<T, Projection> outerKeySelector,
FakeFunc<Table, Projection> innerKeySelector,
FakeFunc<T, Table, U> resultSelector){
var join = new JoinedTable(table, inner,
new EqualsCondition(outerKeySelector(aliases), innerKeySelector(inner)));
var newAliases = resultSelector(aliases, inner);
return new IntermediateJoin<U>(join, newAliases);
}
}
и этот метод для Table
класса:
public IntermediateJoin<T> Join<T>(Table inner,
FakeFunc<Table, Projection> outerKeySelector,
FakeFunc<Table, Projection> innerKeySelector,
FakeFunc<Table, Table, T> resultSelector){
var join = new JoinedTable(this, inner,
new EqualsCondition(outerKeySelector(this), innerKeySelector(inner)));
var x = resultSelector(this, inner);
return new IntermediateJoin<T>(join, x);
}
Это обеспечивает полностью функциональный синтаксис объединения! *
Еще раз спасибо Томашу Петричеку за то, что он нашел время, чтобы прочитать и понять мой вопрос, и дал мне вдумчивый ответ.
* GroupJoin
и SelectMany
еще впереди, но я думаю, что знаю секреты достаточно хорошо, чтобы справиться с ними, сейчас.