その引数，本当にList<T>が最適ですか？ [C#]

この記事の続きです

https://qiita.com/hysui/items/1fa7146f36fb52c5ea50

趣旨としては同じく，何も考えずに`List<T>`を使うのをやめようという内容ですが，今回はまた違ったケースでの話です

この記事では，メソッドの引数で`List<T>`を使うことについて述べていきます

:::note warn
- 記事中のコードは説明を目的とした理解促進のためのものであり，動作検証はしていません
- この記事は一般的な内容であり，パフォーマンスを追求すると型を限定することが最適となる場合もあります．型の限定が一概に悪であるわけではありません
:::


# TL;DR
メソッドの引数には基本的に`List<T>`や配列は使わず，メソッドが必要とする操作に応じて適切なインタフェイスを使おう

# `List<T>`を使うべきなのはどのようなときか

`List<T>`を使うべき場面はあまり多くありません．例として，以下のような場合があります

- 外部ライブラリを使用しており，メソッド内で使用する外部ライブラリのメソッドの引数が`List<T>`である場合など，やむを得ない場合
- `List<T>`固有の操作を行いたい場合[^1]
- 単にそのほうがパフォーマンスに優れている場合

# `List<T>`を使うべきではないのはどのようなときか

我々が`List<T>`を使いたいと考えるとき，実際に必要としているのは`List<T>`ではなく，引数の型を限定する必要はないことが多いです．例として，以下のような場合があります

## 単に複数のアイテムを受け取りたい場合

```cs:Before.cs
// 複数の整数を受け取り，その中に偶数が含まれていればtrueを返すメソッド
bool ContainsEven(List<int> numbers)
{
    foreach (var number in numbers)
    {
        if (number % 2 == 0)
        {
            return true;
        }
    }
    return false;
}
```

例では，引数`numbers`に求める条件は｢`foreach`でループできる複数のint｣であることです．例示したメソッドの引数は`IEnumerable<int>`とするのが適切です

```cs:After.cs
// 複数の整数を受け取り，その中に偶数が含まれていればtrueを返すメソッド
bool ContainsEven(IEnumerable<int> numbers)
{
    foreach (var number in numbers)
    {
        if (number % 2 == 0)
        {
            return true;
        }
    }
    return false;
}
```

さらに，メソッド内でLINQなどの遅延評価を使用する場合，引数を`IEnumerable<T>`とすることで遅延評価を受け入れることができるため，パフォーマンスの向上が期待できます

このように，コレクションを受け取って`foreach`で列挙したいのであれば`IEnumerable<T>`を使いましょう

## 確定済みのコレクションを受け取りたい場合

```cs:Before.cs
// 複数の整数を受け取り，要素が10個以上あればすべての要素を削除するメソッド
void ClearIfHasTenOrGreaterItems(List<int> numbers)
{
    if (numbers.Count >= 10)
    {
        numbers.Clear();
    }
}
```

例では，引数`numbers`に求める条件は｢要素数を取得でき，要素を削除できる複数のint｣であることです．例示したメソッドの引数は`ICollection<int>`とするのが適切です

```cs:After.cs
// 複数の整数を受け取り，要素が10個以上あればすべての要素を削除するメソッド
void ClearIfHasTenOrGreaterItems(ICollection<int> numbers)
{
    if (numbers.Count >= 10)
    {
        numbers.Clear();
    }
}
```

<details><summary>(補足) ｢確定済み｣およびLINQの<code>Count()</code>メソッドについて</summary>

:::note info
- `IEnumerable<T>`では，内容が確定していないコレクションや，無限の要素数を持つコレクションが渡されることがあります
  例えば，インスタンスを`foreach`でループさせるとbreakするまで延々とランダムな整数を返し続ける型を作ることができます
  こういったものはそもそも追加や削除の概念がありません
- メソッド内でコレクションの要素数を使いたい場合，`IEnumerable<T>`では要素数が意味を持たなかったり無限だったりするコレクションが存在しうるため，LINQの`Count()`メソッドがあるからといって引数を`IEnumerable<T>`とするのは危険です
  要素数が欲しい場合は`ICollection<T>`の`Count`プロパティを使うべきです
:::

</details>

このように，順番に意味のないコレクションを受け取って要素を追加や削除したいのであれば`ICollection<T>`を使いましょう

## 順番に意味のあるコレクションを受け取りたい場合

```cs:Before.cs
// 複数の整数を受け取り，最後のアイテムと一致するアイテムを1つ削除するメソッド
void RemoveLast(List<int> numbers)
{
    var last = numbers[^1];
    numbers.Remove(last);
}
```

例では，引数`numbers`に求める条件は｢インデックスでアクセス可能で，要素を削除できる複数のint｣であることです．例示したメソッドの引数は`IList<int>`とするのが適切です

```cs:After.cs
// 複数の整数を受け取り，最後のアイテムと一致するアイテムを1つ削除するメソッド
void RemoveLast(IList<int> numbers)
{
    var last = numbers[^1];
    numbers.Remove(last);
}
```

このように，コレクションを受け取って要素の順番やインデックスを使った処理をしたいのであれば`IList<T>`を使いましょう

## 受け取ったコレクションの内容をメソッド内で変更しない場合

これまで，(書き換えの概念がない`IEnumerable<T>`を除き)受け取ったコレクションの書き換えを伴う場合を見てきました
`IEnumerable<T>`を除き，コレクションのインタフェイスにはそれぞれ対応する読み取り専用のインタフェイスが存在します．これらのインタフェイスは要素を追加したり削除したりするメソッドをもたず，書き換えもできません
引数の型を読み取り専用インタフェイスとすることで，(ズルをしない限り)渡したコレクションがメソッドの中で変更されないことを使用者に明確に伝えることができます

<details><summary>(補足) ズルについて</summary>

:::note info
多くの場合，例えば以下のようなズルをすることで読み取り専用インタフェイスのコレクションを変更することができます

```cs:EvilMethod.cs
void Zuru(IReadOnlyList<int> numbers)
{
    var list = (IList<int>)numbers;
    list.Clear();
}
```

本当に何の意味もないので絶対にこんなことをしてはいけませんが，読み取り専用インタフェイスの意味がわかっていないとこういうことをしてしまうおそれがあります
:::


</details>


```cs:Before.cs
// 複数の整数を受け取り，要素が10個以上あればtrueを返すメソッド
bool HasTenOrGreaterItems(List<int> numbers)
{
    return numbers.Count >= 10;
}
// 複数の整数を受け取り，最後のアイテムを返すメソッド
int Last(List<int> numbers)
{
    return numbers[^1];
}
```

`ICollection<T>`の読み取り専用バージョンは`IReadOnlyCollection<T>`，`IList`の読み取り専用バージョンは`IReadOnlyList<T>`です

```cs:After.cs
// 複数の整数を受け取り，要素が10個以上あればtrueを返すメソッド
bool HasTenOrGreaterItems(IReadOnlyCollection<int> numbers)
{
    return numbers.Count >= 10;
}
// 複数の整数を受け取り，最後のアイテムを返すメソッド
int Last(IReadOnlyList<int> numbers)
{
    return numbers[^1];
}
```

`List<T>`が実装しないためこの記事では扱いませんでしたが，要素が重複しないコレクションである`ISet<T>`の読み取り専用バージョンは`IReadOnlySet<T>`，辞書を表す`IDictionary<TKey, TValue>`の読み取り専用バージョンは`IReadOnlyDictionary<TKey, TValue>`です

# まとめ

ここまでコーディングの面から説明してきましたが，不必要に引数の型を限定しないことで，呼び出す際に不要な変換作業による負荷を強いることがなくなり，パフォーマンス改善の効果が期待できることもあります

この記事ではメソッドの引数を例として扱いましたが，例えば外部から書き換えられたくないpublicなコレクションのプロパティを考えると，似たような考え方で外部からの操作を読み取り専用に制限できます

インタフェイスの利点を理解する上で欠かせないのが，この記事で説明したような｢できる操作｣を起点とした抽象化です
引数の型は，それに対して｢どんな操作ができること｣を求めるのかよく考えて決めましょう

[^1]: 例えば，リストの末尾に複数のアイテムを追加する`AddRange(IEnumerable<T>)`メソッドは`List<T>`の機能なので`List<T>`を使う必要があります
また，例えば`List<T>`と`HashSet<T>`では重複を許さないなどの違いがあるため，そういった違いの影響によってこの記事にある内容だけでは使うべき型を判断できない場合もあります