LINQを使ったデータの取り出し

2023年1月27日

LINQの基礎を習得したら今度は応用してみましょう

ここでは、次の知識を持っていることを前提としています

データの塊から必要なものを取り出す

必要な知識

  • 変数
  • スコープ(ローカル変数の考え方)
  • for文
  • foreach文(知識があれば)
  • クラスの考え方
  • LISTの考え方
  • ラムダ式(このページにサンプルがあります)

データの塊のサンプルとして、シンプルな電話帳を考えてみましょう

1人ずつの情報をクラスにしてみます
人ごとに必要な情報を、その方の名前、電話番号とします

class Record
{
    public string name;
    public string phoneNumber;
}

1人目のデータを作成してみましょう
山田さんで、電話番号は000-0000になります

Record record1 = new Record();

record1.name = "山田";
record1.phoneNumber = "000-0000";

2人目のデータも作ってみましょう

Record record2 = new Record();

record2.name = "鈴木";
record2.phoneNumber = "111-1111";

2人のデータをもつリストとしてphoneBookを作ってみましょう

List<Record> phoneBook = new List<Record>();

phoneBook.Add(record1);
phoneBook.Add(record2);

同様にデータを追加していきますが、今回はサンプルのため、後1人だけにしておきます
ただ、たまたま1人目と同じ山田さんだったとしましょう

まとめると

Record record1 = new Record();

record1.name = "山田";
record1.phoneNumber = "000-0000";

Record record2 = new Record();

record2.name = "鈴木";
record2.phoneNumber = "111-1111";

List<Record> phoneBook = new List<Record>();

Record record3 = new Record();

record3.name = "山田";
record3.phoneNumber = "222-222";

phoneBook.Add(record1);
phoneBook.Add(record2);
phoneBook.Add(record3);

phoneBook(電話帳)の中から、山田さんのデータを検索して抜き出すコードを作ってみましょう
ただし、2人いますので1人目だけにします

for (int i = 0; i < phoneBook.Count; i++)
{
    if (phoneBook[i].name == "山田")
    {
        Console.WriteLine(phoneBook[i].phoneNumber);
        break;
    }
}

結果

000-0000

foreach文で置き換えると次のようになります
同じ動作です
レコードとして1行(1人分)取り出して、チェックしていきます

foreach (var record in phoneBook)
{
    if (record.name == "山田")
    {
        Console.WriteLine(record.phoneNumber);
        break;
    }
}

結果

000-0000

LINQでの実現方法

まず、if文のところを考えてみましょう
selectRecord変数に取り出した1行(1人)を代入してみましょう
Firstとは”最初”という意味で最初に見つかったものを・・・ということになります
しかし、実際のコードではFirstOrDefaultを使うのがいいでしょう
一致しなかった時にデフォルト値(stringではnull)を返すことができるからです

Record selectRecord = phoneBook.FirstOrDefault(record => record.name == "山田");

まず、foreach文でのif文のところを考えてみましょう

if (record.name == "山田")

recordを引数として、その名前が山田さんとイコールならば・・・となるところが似ていますよね
この時のrecordですが、ローカルで使っている変数なので、xでもyでも構いません
でも、名前付けは重要です。意味がわかる名前がいいでしょう。ローカル変数なので他で使っていても影響がありません

record => (record.name == “山田”) // recordを渡して、その名前が山田ならば・・・

あとは、その人の電話番号を表示するだけです

Console.WriteLine(selectRecord.phoneNumber);

結果

000-0000

これで、この電話帳に何人登録されていても大丈夫ですよね!

参考

全てのコード

class Record
{
    public string name;
    public string phoneNumber;
}

class MyClass
{

    public static void Main()
    {
        Record record1 = new Record();

        record1.name = "山田";
        record1.phoneNumber = "000-0000";

        Record record2 = new Record();

        record2.name = "鈴木";
        record2.phoneNumber = "111-1111";

        List<Record> phoneBook = new List<Record>();

        Record record3 = new Record();

        record3.name = "山田";
        record3.phoneNumber = "222-222";

        phoneBook.Add(record1);
        phoneBook.Add(record2);
        phoneBook.Add(record3);

        for (int i = 0; i < phoneBook.Count; i++)
        {
            if (phoneBook[i].name == "山田")
            {
                Console.WriteLine(phoneBook[i].phoneNumber);
                break;
            }
        }

        foreach (var record in phoneBook)
        {
            if (record.name == "山田")
            {
                Console.WriteLine(record.phoneNumber);
                break;
            }
        }

        Record selectRecord = phoneBook.FirstOrDefault(record => record.name == "山田");

        Console.WriteLine(selectRecord.phoneNumber);
    }
}

結果

000-0000
000-0000
000-0000

整理したコード

class Record
{
    public string name;
    public string phoneNumber;
}

class MyClass
{
    static List<Record> phoneBook = new List<Record>();

    static void Main()
    {
        SetRecord("山田", "000-0000");
        SetRecord("鈴木", "111-1111");
        SetRecord("山田", "222-2222");

        FindNumber("山田");
    }

    static void SetRecord(string name, string phoneNumber)
    {
        Record record = new()
        {
            name = name,
            phoneNumber = phoneNumber
        };

        phoneBook.Add(record);
    }

    static void FindNumber(string name)
    {
        Record selectRecord = phoneBook.FirstOrDefault(record => record.name == name);

        Console.WriteLine(selectRecord.phoneNumber);
    }
}

結果

000-0000

実際に運用で使えるように加工したコード

class Record
{
    // 型名に?が付くと「nullが入る可能性があります・・」を表すことになります
    public string? name;
    public string? phoneNumber;
}

static class PhoneBookManager
{
    static List<Record> phoneBook = new List<Record>();

    public static void AddSetRecord(string name, string phoneNumber)
    {
        Record record = new()
        {
            name = name,
            phoneNumber = phoneNumber
        };

        phoneBook.Add(record);
    }

    public static string? FindNumber(string name)
    {
        return phoneBook.FirstOrDefault(record => record.name == name).phoneNumber;
    }
}

class MyClass
{
    static void Main()
    {
        PhoneBookManager.AddSetRecord("山田", "000-0000");
        PhoneBookManager.AddSetRecord("鈴木", "111-1111");
        PhoneBookManager.AddSetRecord("山田", "222-2222");

        Console.WriteLine(PhoneBookManager.FindNumber("山田"));
    }
}

結果

000-0000

(参考)Dictionaryを使った場合

Dictionaryを習った方だとこのように作れるのでは?と思うでしょう
名前と電話番号の紐付けが今後も変更されないのであれば、全く問題ありません

Listとクラスを使った用途としては、

  • 今後、レコードのメンバーが増えていくことが考えられる(郵便番号欄とか住所など)
  • 同じ名前が存在する(Dictionaryはキーの重複が許されていません)
  • UnityでUnityJsonを使って、ファイルに保存する

最後のJsonのところは、Unityのクラスに特化した条件です。デフォルトでは対応していませんが変換はできます

static class PhoneBookManager
{
    static Dictionary<string, string> phoneBook = new();

    public static void AddSetRecord(string name, string phoneNumber)
    {
        phoneBook.Add(name, phoneNumber);
    }

    public static string? FindNumber(string name)
    {
        return phoneBook[name];
    }
}

class MyClass
{
    static void Main()
    {
        PhoneBookManager.AddSetRecord("山田", "000-0000");
        PhoneBookManager.AddSetRecord("鈴木", "111-1111");

        Console.WriteLine(PhoneBookManager.FindNumber("山田"));
    }
}

結果

000-0000

(参考:応用)この電話帳を1つだけプロジェクトに存在するようにしたい(現在staticクラスで実現)また、継承を利用した派生クラスを作りたい(staticクラスの場合、これができない)

このような要求に応えるには、シングルトンパターンを使います

  • new 演算子で新しくインスタンスは作れません
  • staticクラスと違いインスタンスとして作れられます
  • staticとの違いによるメリットとして
    • Singletonパターンであればクラスへの参照ではなくオブジェクトへの参照となります
    • そのため、staticではないメソッドの参照も可能となります
    • 継承・オーバーライドができます
    • インターフェースを実装することができます

staticなクラスは継承が行えないため、Singletonパターンで求められるような派生クラスごとに実装を作り分ける柔軟性が持てないといえます

class PhoneBookManager
{
    public static PhoneBookManager Instance { get; } = new();

    PhoneBookManager() { }

    public Dictionary<string, string> PhoneBook { get; set; } = new();
}

class MyClass
{
    static void Main()
    {
        PhoneBookManager.Instance.PhoneBook.Add("山田", "000-0000");
        PhoneBookManager.Instance.PhoneBook.Add("鈴木", "111-1111");

        Console.WriteLine(PhoneBookManager.Instance.PhoneBook["山田"]);
    }
}

結果

000-0000

確認1(エラーになる場合)

new 演算子でインスタンスを複数作れません(エラーになります)
これにより、複数のインスタンスを作ること自体できないことになります

PhoneBookManager phoneBookManager = new();

‘コンストラクタのアクセス就職子は、privateになっています(省略するとprivate)
privateは、外部からアクセスできません(保護レベルによりアクセスできません)

PhoneBookManager() { }

クラス・メンバーのアクセス修飾子は、そのメンバーにアクセスできないようにするものです。詳細については、アクセス修飾子を参照してください。
この原因のひとつは、フレンドアセンブリのターゲットで /out コンパイラフラグが省略されていることです(下のサンプルでは示されていません)。詳細は、「フレンドアセンブリとOutputAssembly(C#コンパイラオプション)」を参照してください。

確認2(エラーになる場合)

次のようなコードを使ってInstance変数に代入しようとしてもできないエラーになります

PhoneBookManager.Instance = null;

getのみを持ったプロパティに代入はできません

public static PhoneBookManager Instance { get; } = new();