LoginSignup
21
15

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

Last updated at Posted at 2024-03-13

この記事の続きです

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

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

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

TL;DR

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

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

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

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

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

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

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

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>とするのが適切です

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>を使いましょう

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

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

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

After.cs
// 複数の整数を受け取り,要素が10個以上あればすべての要素を削除するメソッド
void ClearIfHasTenOrGreaterItems(ICollection<int> numbers)
{
    if (numbers.Count >= 10)
    {
        numbers.Clear();
    }
}
(補足) 「確定済み」およびLINQのCount()メソッドについて
  • IEnumerable<T>では,内容が確定していないコレクションや,無限の要素数を持つコレクションが渡されることがあります
    例えば,インスタンスをforeachでループさせるとbreakするまで延々とランダムな整数を返し続ける型を作ることができます
    こういったものはそもそも追加や削除の概念がありません
  • メソッド内でコレクションの要素数を使いたい場合,IEnumerable<T>では要素数が意味を持たなかったり無限だったりするコレクションが存在しうるため,LINQのCount()メソッドがあるからといって引数をIEnumerable<T>とするのは危険です
    要素数が欲しい場合はICollection<T>Countプロパティを使うべきです

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

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

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

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

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

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

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

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

(補足) ズルについて

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

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

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

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>です

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>では重複を許さないなどの違いがあるため,そういった違いの影響によってこの記事にある内容だけでは使うべき型を判断できない場合もあります

21
15
17

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
21
15