iT邦幫忙

2018 iT 邦幫忙鐵人賽
DAY 20
1
Software Development

深入探索LINQ系列 第 20

C#的利器LINQ-Join的應用

在資料表的設計中我們會將資料分門別類,例如說人的資料是一張表,電話是一張表,然後會有一個ID關聯兩張表,這時我們如果要找某個人有哪些連絡電話,就會使用到Join的語法來合併人及電話的資料,藉此找到此人對應的聯絡電話。

LINQ中也有Join這個方法,是要如何使用呢? 讓我們一起來看看吧。

功能說明

設定OuterInner兩個資料型別物件,再將兩個型別中對應對方的屬性訂出來,最後決定輸出的資料結構,取得目標資料。

方法定義

Join有兩個公開方法如下:

public static IEnumerable<TResult> Join<TOuter, TInner, TKey, TResult>(
    this IEnumerable<TOuter> outer,
    IEnumerable<TInner> inner,
    Func<TOuter, TKey> outerKeySelector,
    Func<TInner, TKey> innerKeySelector,
    Func<TOuter, TInner, TResult> resultSelector);

public static IEnumerable<TResult> Join<TOuter, TInner, TKey, TResult>(
    this IEnumerable<TOuter> outer,
    IEnumerable<TInner> inner,
    Func<TOuter, TKey> outerKeySelector,
    Func<TInner, TKey> innerKeySelector,
    Func<TOuter, TInner, TResult> resultSelector,
    IEqualityComparer<TKey> comparer);

下面依序解說每個參數的意義:

  • outer: 要收束的資料
  • inner: 期望outer要有的資料
  • outerKeySelector: 跟inner有關聯的屬性
  • innerKeySelector: 跟outer有關聯的屬性
  • resultSelector: 目標資料
  • comparer: innerouter關聯屬性的等值比較器

我們用剛剛提到的電話的例子來看,我們要找到某個人的電話,可以畫成下面的這張圖:

https://ithelp.ithome.com.tw/upload/images/20180108/20107789OG1s5XgkeI.png

可以看到因為我們的目標是特定電話號碼,所以inner,而電話outer,但是因為LINQ的Join方法是Inner Join,如果想要找的人沒有電話資訊,那個人的資料也不會出現,因此圖片的人的圓圈才會畫到外面。

查詢運算式

依據C# Spec,我們可以看到join的定義如下:

join_clause
    : 'join' type? identifier 'in' expression 'on' expression 'equals' expression
    ;

這個定義看不出個所以然,那我們用Northwind裡的資料來寫個例子:

找出所有有訂單的客戶聯絡人姓名

from c in Customers
join o in Orders on c.CustomerID equals o.CustomerID
select c.ContactName

可以轉為下面的方法寫法:

Customers
   .Join (
      Orders,
      c => c.CustomerID,
      o => o.CustomerID,
      (c, o) => c.ContactName
   )
  • outer: Customers
  • inner: Orders
  • outerKeySelector: Customers.CustomerID
  • innerKeySelector: Orders.CustomerID
  • resultSelector: Customers.ContactName

有了這個例子就清楚多了,from指定的是outer,而join指定的是inner,後面的equalsinnerouter關聯屬性的設定。

接著我們就可以來看運算式及方法的轉換公式了。

下面是運算式:

from x1 in e1
join x2 in e2 on k1 equals k2
select v

可以被轉為:

( e1 ) . Join( e2 , x1 => k1 , x2 => k2 , ( x1 , x2 ) => v )

方法範例

範例資料結構如下:

class Person
{
    public string Name { get; set; }
}

class Phone
{
    public string PhoneNumber { get; set; }
    public Person Person { get; set; }
}

範例資料如下:

Person Peter = new Person() { Name = "Peter" };
Person Sunny = new Person() { Name = "Sunny" };
Person Tim = new Person() { Name = "Tim" };
Person May = new Person() { Name = "May" };

Phone num1 = new Phone() { PhoneNumber = "01-5555555", Person = Peter };
Phone num2 = new Phone() { PhoneNumber = "02-5555555", Person = Sunny };
Phone num3 = new Phone() { PhoneNumber = "03-5555555", Person = Tim };
Phone num4 = new Phone() { PhoneNumber = "04-5555555", Person = May };
Phone num5 = new Phone() { PhoneNumber = "05-5555555", Person = Peter };

下列範例採用上面資料來演繹。

找出人名跟電話號碼的對應資料

Phone[] phones = new Phone[] { num1, num2, num3, num4, num5 };
Person[] persons = new Person[] { Peter, Sunny, Tim, May };

var results = persons.Join(
    phones,
    person => person,
    phone => phone.Person,
    (person, phone) => new { name = person.Name, phoneNumber = phone.PhoneNumber });

foreach (var result in results)
{
    Console.WriteLine($"{result.name}: {result.phoneNumber}");
}

/*
 * output:
 *
 * Peter: 01-5555555
 * Peter: 05-5555555
 * Sunny: 02-5555555
 * Tim: 03-5555555
 * May: 04-5555555
 */

這裡我們注意到它的順序是依照outer的順序排序的,如果同一個outer複數inner資料,才會依照inner順序排列。

Join是Inner Join

我們將PersonPhone的資料各拿掉一個,會是互相有對應到的資料才會輸出。

Phone[] phones = new Phone[] { num1, num2, num3, num4, num5 };
Person[] persons = new Person[] { Peter, Sunny, Tim, May };

IEnumerable<Person> skipPersons = persons.Skip(1);
var results = skipPersons.Join(phones,
                person => person,
                phone => phone.Person,
                (person, phone) => new { name = person.Name, phoneNumber = phone.PhoneNumber });

/*
 * output:
 * Sunny: 02-5555555
 * Tim: 03-5555555
 * May: 04-5555555
 */

IEnumerable<Phone> skipPhones = phones.Skip(1);
var results = persons.Join(skipPhones,
                person => person,
                phone => phone.Person,
                (person, phone) => new { name = person.Name, phoneNumber = phone.PhoneNumber });

/*
 * output:
 * Peter: 05-5555555
 * Sunny: 02-5555555
 * Tim: 03-5555555
 * May: 04-5555555
 */

客製比較器

現在有一個奇怪的需求: 姓名最後一個字母相同的話電話可以共用

我們試試用客製比較器來完成:

var results = persons.Join(phones,
                person => person,
                phone => phone.Person,
                (person, phone) => new { name = person.Name, phoneNumber = phone.PhoneNumber },
                new CustomComparer());
...
class CustomComparer : IEqualityComparer<Person>
{
    public bool Equals(Person x, Person y)
    {
        return x.Name.TakeLast(1).FirstOrDefault() == y.Name.TakeLast(1).FirstOrDefault();
    }
    public int GetHashCode(Person obj)
    {
        return obj.Name.TakeLast(1).FirstOrDefault().GetHashCode();
    }
}

/*
 * output:
 * Peter: 01-5555555
 * Peter: 05-5555555
 * Sunny: 02-5555555
 * Sunny: 04-5555555
 * Tim: 03-5555555
 * May: 02-5555555
 * May: 04-5555555
 */

我們可以看到SunnyMay因為最後一個字母都是y,所以他們所對應的電話都有對方的號碼。

特別之處

  • 是延遲執行的方法
  • 輸出資料的排序會是先outerinner
  • 沒有傳入客製比較器,則用Default比較器

結語

Join因為是Inner Join,所以對於要拿取的資料來說,inner及outer是沒有差別的,但是剛剛提到的排序就會有差別,如果對排序有需求的資料還是要小心使用。

範例程式

GitHub

參考


上一篇
C#的利器LINQ-GroupBy的原碼探索
下一篇
C#的利器LINQ-Join的原碼探索
系列文
深入探索LINQ30

尚未有邦友留言

立即登入留言