Ваш собственный ответ в порядке, но он не очень элегантен. Так что да, это немного грязно. Существует стандартный способ выполнения левого внешнего соединения, который обрабатывает ваш пример , а обрабатывает случаи, когда есть повторяющиеся города. Ваш пример не может обрабатывать дубликаты городов, потому что любые дубликаты игнорируются при выборе c2s.First()
.
Стандартные шаги левого соединения таковы:
- Создайте иерархию из ваших данных с помощью GroupJoin.
- Свести иерархию с помощью SelectMany.
Ваш GroupJoin выравнивает иерархию за один шаг, игнорируя все, кроме первого соответствующего города. Вот что в этом грязного. Если бы вы попытались использовать этот код в обратном порядке, взяв города и оставив их соединенными с местами, вы бы получили только одно место в каждом городе! Это явно плохо. Лучше научиться правильно соединять левые, и тогда это всегда будет работать.
SelectMany на шаге 2 на самом деле не обязателен, если вы предпочитаете сохранить иерархию и затем использовать вложенные циклы foreach для их отображения, но я собираюсь предположить, что вы хотите отобразить данные в формате плоской таблицы.
Если вы просто хотите увидеть ответ на конкретную проблему, прокрутите вниз до заголовка «Города и места» ниже, но сначала приведем полный пример с использованием двух простых строковых массивов.
Абстрактный пример с полным объяснением
Вот полный пример использования двух массивов букв вместо вашего кода. Сначала я хотел показать более простой пример. Вы можете скопировать и вставить это в LINQPad, установить для языка «C # Statements» и запустить его для себя, если хотите. Я высоко рекомендую LINQPad в качестве инструмента для тестирования всех видов кода, а не только LINQ. Кроме того, вы также можете создать консольное приложение в Visual Studio.
Вот код без лишних комментариев. Ниже это версия, которая сильно аннотирована. Вы можете перейти к этому, если хотите точно узнать, что означает каждый параметр.
var leftLetters = new string[]{ "A", "B", "C" };
var rightLetters = new string[]{ "A", "B" };
//Create a hierarchical collection that includes every left item paired with a collection of matching right items (which may be empty if there are no matching right items.)
var groupJoin =
leftLetters.GroupJoin(
rightLetters,
leftLetter => leftLetter,
rightLetter => rightLetter,
( leftLetter, matchingRightLetters ) => new { leftLetter, matchingRightLetters }
);
//Flatten the groupJoin hierarchical collection with a SelectMany
var selectMany =
groupJoin.SelectMany(
groupJoinItem => groupJoinItem.matchingRightLetters.DefaultIfEmpty( "MISSING" ),
( groupJoinItem, rightLetter ) => new {
LeftLetter = groupJoinItem.leftLetter,
RightLetter = rightLetter
}
);
//You can think of the elements of selectMany as "rows" as if this had been a left outer join in SQL. But this analogy breaks down rapidly if you are selecting objects instead of scalar values.
foreach( var row in selectMany )
{
Console.WriteLine( row.LeftLetter + ", " + row.RightLetter );
}
Вот вывод, который должен быть довольно очевидным, так как мы все знаем, что должно делать левое соединение.
A, A
B, B
C, MISSING
сильно аннотированная версия:
var leftLetters = new string[]{ "A", "B", "C" };
var rightLetters = new string[]{ "A", "B" };
//Create a hierarchical collection that includes every left item paired with a collection of matching right items (which may be empty if there are no matching right items.)
var groupJoin =
leftLetters.GroupJoin(
rightLetters, //inner: the right hand collection in the join
leftLetter => leftLetter, //outerKeySelector: There is no property to use as they join key, the letter *is* the key. So this lambda simply returns the parameter itself.
rightLetter => rightLetter, //innerKeySelector: Same with the rightLetters
( leftLetter, matchingRightLetters ) => new { leftLetter, matchingRightLetters } //resultSelector: given an element from the left items, and its matching collection of right items, project them to some class. In this case we are using a new anonymous type.
);
//Flatten the groupJoin hierarchical collection with a SelectMany
var selectMany =
groupJoin.SelectMany(
//collectionSelector: given a single element from our collection of group join items from above, provide a collection of its "right" items which we want to flatten out. In this case the right items are in a property of the groupJoinItem itself, but this does not need to be the case! We use DefaultIfEmpty to turn an empty collection into a new collection that has exactly one item instead: the string "MISSING".
groupJoinItem => groupJoinItem.matchingRightLetters.DefaultIfEmpty( "MISSING" ),
//resultSelector: SelectMany does the flattening for us and this lambda gets invoked once for *each right item* in a given left item's collection of right items.
(
groupJoinItem, //The first parameter is one of the original group join item, including its entire collection of right items, but we will ignore that collection in the body of this lamda and just grab the leftLetter property.
rightLetter //The second parameter is *one* of the matching right items from the collection of right items we selected in the first lambda we passed into SelectMany.
)
=> new {
LeftLetter = groupJoinItem.leftLetter, //groupJoinItem is one of the original items from the GroupJoin above. We just want the left letter from it.
RightLetter = rightLetter //This is one of the individual right letters, so just select it as-is.
}
);
//You can think of the elements of selectMany as "rows" as if this had been a left outer join in SQL. But this analogy breaks down rapidly if you are selecting objects instead of scalar values.
foreach( var row in selectMany )
{
Console.WriteLine( row.LeftLetter + ", " + row.RightLetter );
}
Опять вывод для справки:
A, A
B, B
C, MISSING
Вышеуказанное использование LINQ часто называют «цепочкой методов». Вы берете некоторые коллекции и объединяете методы, чтобы получить то, что вы хотите (В большинстве случаев вы не используете переменные для хранения отдельных выражений. Вы просто делаете GroupJoin (...). SelectMany (...), поэтому и называется «цепочкой методов». Это очень многословно и явно, и долго писать.
Вместо этого мы можем использовать то, что называется «понимание», «понимание запроса» или «понимание LINQ». Понимание - это старый термин информатики 1970-х годов, который, честно говоря, не имеет большого смысла для большинства людей. Вместо этого люди называют их «запросами LINQ» или «выражениями LINQ», но они технически применимы и к цепочкам методов, потому что в обоих случаях вы строите дерево выражений. (Деревья выражений выходят за рамки данного руководства.) Понимание LINQ - это SQL-подобный синтаксис для написания LINQ, но это не SQL! Это не имеет ничего общего с реальным SQL. Вот тот же код, написанный для понимания запросов:
var leftLetters = new string[]{ "A", "B", "C" };
var rightLetters = new string[]{ "A", "B" };
var query =
from leftLetter in leftLetters
join rightLetter in rightLetters
on leftLetter equals rightLetter into matchingRightLetters
from rightLetter in matchingRightLetters.DefaultIfEmpty( "MISSING" )
select new
{
LeftLetter = leftLetter,
RightLetter = rightLetter
};
foreach( var row in query )
{
Console.WriteLine( row.LeftLetter + ", " + row.RightLetter );
}
Это скомпилирует в точный тот же код, что и в примере выше, за исключением того, что параметр с именем "groupJoinItem" в SelectMany будет иметь имя, похожее на "temp0", поскольку этот параметр явно не существует в понятной версии этого кода.
Я думаю, вы можете оценить, насколько проще эта версия кода. Я всегда использую этот синтаксис при выполнении левого внешнего соединения. Я никогда не использую GroupJoin с SelectMany. Однако, на первый взгляд, в этом мало смысла. join
, за которым следует into
, создает GroupJoin. Сначала вы должны знать это, и почему вы хотите это. Затем второй from
указывает на SelectMany, что неочевидно. Когда у вас есть два from
ключевых слова, вы фактически создаете перекрестное объединение (декартово произведение), что и делает SelectMany. (Вроде.)
Например, этот запрос:
from leftLetter in leftLetters
from rightLetter in rightLetters
select new
{
LeftLetter = leftLetter,
RightLetter = rightLetter
}
даст:
A, A
A, B
B, A
B, B
C, A
C, B
Это базовое перекрестное соединение.
Итак, вернемся к нашему исходному запросу LINQ для левого соединения: первый from
запроса - это групповое соединение, а второй from
выражает перекрестное соединение между каждым groupJoinItem и его собственной коллекцией соответствующих правых букв. Это примерно так:
from groupJoinItem in groupJoin
from rightLetter in groupJoinItem.matchingRightLetters
select new{...}
На самом деле, мы могли бы написать это так!
var groupJoin =
from leftLetter in leftLetters
join rightLetter in rightLetters
on leftLetter equals rightLetter into matchingRightLetters
select new
{
LeftLetter = leftLetter,
MatchingRightLetters = matchingRightLetters
};
var selectMany =
from groupJoinItem in groupJoin
from rightLetter in groupJoinItem.MatchingRightLetters.DefaultIfEmpty( "MISSING" )
select new
{
LeftLetter = groupJoinItem.LeftLetter,
RightLetter = rightLetter
};
Это selectMany выражает следующее: «для каждого элемента в groupJoin перекрестно соедините его с его собственным свойством MatchingRightLetters и объедините все результаты вместе». Это дает тот же результат, что и любой из наших кодов левого соединения выше.
Это, вероятно, слишком много объяснения для этого простого вопроса, но мне не нравится программирование культа грузов (Google google). Вы должны точно знать , что делает ваш код и почему, иначе вы не сможете решать более сложные проблемы.
Города и Места
Итак, вот версия цепочки методов вашего кода. Это целая программа, поэтому люди могут запускать ее, если захотят (используйте языковой тип "C # Program" в LINQPad или создайте консольное приложение с Visual Studio или компилятором C #.)
void Main()
{
City[] cities = new City[]{
new City{CityCode="0771",CityName="Raipur",CityPopulation="BIG"},
new City{CityCode="0751",CityName="Gwalior",CityPopulation="MEDIUM"},
new City{CityCode="0755",CityName="Bhopal",CityPopulation="BIG"},
new City{CityCode="022",CityName="Mumbai",CityPopulation="BIG"},
};
CityPlace[] places = new CityPlace[]{
new CityPlace{CityCode="0771",Place="Shankar Nagar"},
new CityPlace{CityCode="0771",Place="Pandari"},
new CityPlace{CityCode="0771",Place="Energy Park"},
new CityPlace{CityCode="0751",Place="Baadaa"},
new CityPlace{CityCode="0751",Place="Nai Sadak"},
new CityPlace{CityCode="0751",Place="Jayendraganj"},
new CityPlace{CityCode="0751",Place="Vinay Nagar"},
new CityPlace{CityCode="0755",Place="Idgah Hills"},
new CityPlace{CityCode="022",Place="Parel"},
new CityPlace{CityCode="022",Place="Haaji Ali"},
new CityPlace{CityCode="022",Place="Girgaon Beach"},
new CityPlace{CityCode="0783",Place="Railway Station"}
};
var query =
places.GroupJoin(
cities,
place => place.CityCode,
city => city.CityCode,
( place, matchingCities )
=> new {
place,
matchingCities
}
).SelectMany(
groupJoinItem => groupJoinItem.matchingCities.DefaultIfEmpty( new City{ CityName = "NO NAME" } ),
( groupJoinItem, city )
=> new {
Place = groupJoinItem.place,
City = city
}
);
foreach(var pair in query)
{
Console.WriteLine( pair.Place.Place + ": " + pair.City.CityName );
}
}
class City
{
public string CityCode;
public string CityName;
public string CityPopulation;
}
class CityPlace
{
public string CityCode;
public string Place;
}
Вот вывод:
Shankar Nagar: Raipur
Pandari: Raipur
Energy Park: Raipur
Baadaa: Gwalior
Nai Sadak: Gwalior
Jayendraganj: Gwalior
Vinay Nagar: Gwalior
Idgah Hills: Bhopal
Parel: Mumbai
Haaji Ali: Mumbai
Girgaon Beach: Mumbai
Railway Station: NO NAME
Обратите внимание, что DefaultIfEmpty будет возвращать новый экземпляр фактического класса City, а не просто строку. Это потому, что мы соединяем CityPlaces с реальными объектами City, а не со строками. Вместо этого вы можете использовать DefaultIfEmpty()
без параметров, и вы получите null
City для «Железнодорожного вокзала», но тогда вам придется проверять наличие нулей в цикле foreach перед вызовом pair.City.CityName. Это вопрос личных предпочтений.
Вот та же программа, использующая понимание запросов:
void Main()
{
City[] cities = new City[]{
new City{CityCode="0771",CityName="Raipur",CityPopulation="BIG"},
new City{CityCode="0751",CityName="Gwalior",CityPopulation="MEDIUM"},
new City{CityCode="0755",CityName="Bhopal",CityPopulation="BIG"},
new City{CityCode="022",CityName="Mumbai",CityPopulation="BIG"},
};
CityPlace[] places = new CityPlace[]{
new CityPlace{CityCode="0771",Place="Shankar Nagar"},
new CityPlace{CityCode="0771",Place="Pandari"},
new CityPlace{CityCode="0771",Place="Energy Park"},
new CityPlace{CityCode="0751",Place="Baadaa"},
new CityPlace{CityCode="0751",Place="Nai Sadak"},
new CityPlace{CityCode="0751",Place="Jayendraganj"},
new CityPlace{CityCode="0751",Place="Vinay Nagar"},
new CityPlace{CityCode="0755",Place="Idgah Hills"},
new CityPlace{CityCode="022",Place="Parel"},
new CityPlace{CityCode="022",Place="Haaji Ali"},
new CityPlace{CityCode="022",Place="Girgaon Beach"},
new CityPlace{CityCode="0783",Place="Railway Station"}
};
var query =
from place in places
join city in cities
on place.CityCode equals city.CityCode into matchingCities
from city in matchingCities.DefaultIfEmpty( new City{ CityName = "NO NAME" } )
select new {
Place = place,
City = city
};
foreach(var pair in query)
{
Console.WriteLine( pair.Place.Place + ": " + pair.City.CityName );
}
}
class City
{
public string CityCode;
public string CityName;
public string CityPopulation;
}
class CityPlace
{
public string CityCode;
public string Place;
}
Как давний пользователь SQL, я предпочитаю версию для понимания запросов. Кому-то гораздо проще прочитать намерение кода, если вы знаете, что делают отдельные части запроса.
Счастливого программирования!