Мой ответ содержит программу, которая, на мой взгляд, лучше, чем предложенная в ответе Уибла, но сначала я хотел бы продемонстрировать, как работает метод Join
, и рассказать о проблемах, которые я вижу в вашем подходе.
Как вы можете видеть здесь https://docs.microsoft.com/en-us/dotnet/api/system.linq.enumerable.join?view=netframework-4.8 метод Join
Коррелирует элементы двух последовательностей на основе соответствующих ключей.
Если ключи не совпадают, элементы из обеих коллекций не включаются. Например, удалите методы Equals
и GetHashCode
и попробуйте этот код:
var first = new List<Foo> { new Foo(1, 1) };
var second = new List<Foo> { new Foo(1, 1), new Foo(1, 2), new Foo(1, 3) };
//This is your original code that returns no results
var result = second.Join(first, s => s, f => f, (f, s) => new { f, s }).ToList();
result = first.Join(second, s => s, f => f, (f, s) => new { f, s }).ToList();
//This code is mine and it returns in both calls of the Join method one element in the resulting collection; the element contains two instances of Foo (1,1) - f and s
result = second.Join(first, s => new { s.Id, s.NullableId }, f => new { f.Id, f.NullableId }, (f, s) => new { f, s }).ToList();
result = first.Join(second, s => new { s.Id, s.NullableId }, f => new { f.Id, f.NullableId }, (f, s) => new { f, s }).ToList();
Но если вы зададите исходный ввод данных, содержащий null
, с моим кодом:
var first = new List<Foo> { new Foo(1, null) };
var second = new List<Foo> { new Foo(1, 1), new Foo(1, 2), new Foo(1, 3) };
var result = second.Join(first, s => new { s.Id, s.NullableId }, f => new { f.Id, f.NullableId }, (f, s) => new { f, s }).ToList();
result = first.Join(second, s => new { s.Id, s.NullableId }, f => new { f.Id, f.NullableId }, (f, s) => new { f, s }).ToList();
переменная результата будет пустой в обоих случаях, поскольку ключ {1, null
} не соответствует ни одному другому ключу, то есть {1, 1}, {1, 2}, {1, 3}.
Теперь вернемся к вашему вопросу. Я бы посоветовал вам пересмотреть весь ваш подход в таких случаях, и вот почему. Давайте представим, что ваша реализация методов Equals
и GetHashCode
сработала так, как вы ожидали, и вы даже не опубликовали свой вопрос. Тогда ваше решение создает следующие результаты, как я вижу:
Чтобы понять, как ваш код рассчитывает его вывод, пользователь вашего кода должен иметь доступ к коду Foo
напечатайте и проведите время, рассматривая вашу реализацию методов Equals
и GetHashCode
(или читая документацию).
При такой реализации методов Equals
и GetHashCode
вы пытаетесь изменить ожидаемое поведение метода Join
. Пользователь может ожидать, что первый элемент первой коллекции Foo
(1, null) не будет считаться равным первому элементу второй коллекции Foo
(1, 1).
Давайте представим, что у вас есть несколько классов для присоединения, каждый написан каким-то отдельным человеком, и у каждого класса есть свои собственные логики c в методах Equals
и GetHashCode
, Чтобы выяснить, как на самом деле ваше объединение работает с каждым типом, пользователю вместо того, чтобы изучать реализацию метода объединения только один раз, нужно проверить исходный код всех этих классов, пытаясь понять, как каждый тип обрабатывает свое собственное сравнение, сталкиваясь с различными вариациями таких вещей, как это с магическими числами c (взятыми из вашего кода):
public override int GetHashCode()
{
var hashCode = 806340729;
hashCode = hashCode * -1521134295 + Id.GetHashCode();
return hashCode;
}
Это может не показаться большой проблемой, но представьте, что вы - новый человек в проекте, у вас много классов с logi c, подобным этому, и ограниченным временем для выполнения вашей задачи, например, у вас есть срочный запрос на изменение, огромный набор данных для ввода и нет юнит-тестов.
Если кто-то унаследует от вашего класса Foo и поместит экземпляр Foo1 в коллекцию вместе с экземплярами Foo:
public class Foo1 : Foo
{
public Foo1(int id, int? nullableId) : base (id, nullableId)
{
Id = id;
NullableId = nullableId;
}
public override bool Equals(object obj)
{
var otherFoo1 = (Foo1)obj;
return Id == otherFoo1.Id;
}
public override int GetHashCode()
{
var hashCode = 806340729;
hashCode = hashCode * -1521134295 + Id.GetHashCode();
return hashCode;
}
}
var first = new List<Foo> { new Foo1(1, 1) };
var second = new List<Foo> { new Foo(1, 1), new Foo(1, 2), new Foo(1, 3)};
var result = second.Join(first, s => s, f => f, (f, s) => new { f, s }).ToList();
result = first.Join(second, s => s, f => f, (f, s) => new { f, s }).ToList();
, тогда у вас есть среда выполнения исключение в методе Equals
типа Foo1: System.InvalidCastException, Message = Невозможно привести объект типа 'ConsoleApp1.Foo' к типу 'ConsoleApp1.Foo1'. С такими же входными данными мой код будет нормально работать в этой ситуации:
var result = second.Join(first, s => s.Id, f => f.Id, (f, s) => new { f, s }).ToList();
result = first.Join(second, s => s.Id, f => f.Id, (f, s) => new { f, s }).ToList();
С вашей реализацией методов Equals
и GetHashCode
, когда кто-то изменяет код присоединения, например это:
var result = second.Join(first, s => new { s.Id, s.NullableId }, f => new { f.Id, f.NullableId }, (f, s) => new { f, s }).ToList();
result = first.Join(second, s => new { s.Id, s.NullableId }, f => new { f.Id, f.NullableId }, (f, s) => new { f, s }).ToList();
тогда ваши логики c в методах Equals
и GetHashCode
будут игнорироваться и у вас будет другой результат.
На мой взгляд, этот подход (с переопределением Equals
и GetHashCode
методов) может быть источником множества ошибок. Я думаю, что лучше, когда ваш код, выполняющий объединение, имеет реализацию, которая может быть понята без какой-либо дополнительной информации, реализация logi c сосредоточена в одном методе, реализация ясна, предсказуема, поддерживаема, и ее легко Понимаю.
Обратите также внимание, что с вашими входными данными:
var first = new List<Foo> { new Foo(1, null) };
var second = new List<Foo> { new Foo(1, 1), new Foo(1, 2), new Foo(1, 3) };
код в ответе Weeble генерирует следующий вывод:
Foo(1, 1)
Foo(1, 2)
Foo(1, 3)
, насколько я понимаю Вы попросили реализацию, которая с помощью ввода производит вывод, который выглядит следующим образом:
Foo(1, null), Foo(1, 1)
Foo(1, null), Foo(1, 2)
Foo(1, null), Foo(1, 3)
Пожалуйста, рассмотрите возможность обновления вашего решения с помощью моего кода, поскольку он дает результат в формате, который вы запрашивали, мой код легче понять, и у него есть другие преимущества, как вы можете видеть:
using System;
using System.Collections.Generic;
using System.Linq;
namespace ConsoleApp40
{
public class Foo
{
public int Id { get; set; }
public int? NullableId { get; set; }
public Foo(int id, int? nullableId)
{
Id = id;
NullableId = nullableId;
}
public override string ToString() => $"Foo({Id}, {NullableId?.ToString() ?? "null"})";
}
class Program
{
static void Main(string[] args)
{
var first = new List<Foo> { new Foo(1, null), new Foo(1, 5), new Foo(2, 3), new Foo(6, 2) };
var second = new List<Foo> { new Foo(1, 1), new Foo(1, 2), new Foo(1, 3), new Foo(2, null) };
var result = second.Join(first, s=>s.Id, f=>f.Id, (f, s) => new { f, s })
.Where(o => !((o.f.NullableId != null && o.s.NullableId != null) &&
(o.f.NullableId != o.s.NullableId)));
foreach (var o in result) {
Console.WriteLine(o.f + ", " + o.s);
}
Console.ReadLine();
}
}
}
Вывод :
Foo(1, 1), Foo(1, null)
Foo(1, 2), Foo(1, null)
Foo(1, 3), Foo(1, null)
Foo(2, null), Foo(2, 3)