C#:ArrayとListのパンドラの箱を開けてみた

C#

小難しいタイトルですが、なんてことはありません。
ここでお話ししたいことは、ArrayとListがどのようなのインターフェースを実装しているのか、ということです。

そして、最も興味があるインターフェースは、
コレクションの御三家である

  • IEnumerable
  • ICollection
  • IList

の3つです。

中級者ぐらいの方はピンとくるのではないでしょうか。
分からない方に簡単に言うと、
具象クラスであるArrayやListやDictionary, Hashなどのコレクションは上のどれかのコレクションを持っています。
(具象クラスは実際にプログラムで使えるクラスのことです)

それぞれの具象クラスがどのインターフェースを持っているのか、皆さんはきちんと把握できているでしょうか?
ここでは初心者向けにArrayとListを中心にお話ししたいと思います。

実は、ArrayもListも
最上級である、『IListを実装している』ということです。
ただし、Arrayはちょっと無理している実装になっているようです。
では、見ていきましょう。

スポンサードサーチ

コレクション御三家

まず、コレクションについてざっくり説明してみましょう。

コレクションというのは、複数のデータを保持できる機能のこと、と考えてください。
また、ここで取り上げる保持方法は、先ほども言ったようにArray(配列)とListです。
Array(配列)というのは、タンスの引き出しをイメージした保存方法で、
Listは電車のように連結し、それぞれの要素の入れ替えが可能です。

それらコレクションに関して、実はできることの程度がきまっています。
その区分けがIEnumerable, ICollection, IList のインターフェースに
表れていると思ってください。

では、コレクション御三家を見ていきましょう。

IEnumerable

IEnumerableは
コレクションの値を順次を取り出して、評価してくれます。

『評価』と書いてあるとなにやら難しいようですが、そんなことはありません。
長いパイプにたくさんのボールを詰め込み、先端から押し出して、もう片方の先端からボールを取り出すという簡単な作業をしているだけです。
先端から押し出す作業が、値を取り出すことです。これを繰り返します。

そして、値を取り出せなかったときは、押し出す作業をやめ、これで終わりだよ!と教えてくれます。これが評価です。

実際にこの作業を行うのは、GetEnumerator()というメソッドで行われます。
GetEnumeratorはIEnumeratorという型(インターフェース)であり、
まさに上で説明したようなことを行っています。

また、IEnumerableの特徴としてよく言われるのが、
『遅延評価』であることです。

なぜならば、先ほども言ったように、一つ一つ値を取り出すからです。
取り出してみるまで、最後の値なのかどうかも分かりません。

先程のように長いパイプの筒の中にボールが詰まっている状態を想像してみましょう。
長いパイプの中にボールを保持しているため、当然外からはボールは一切見えません。
その都度その都度取りに行かなければ分からないのです。

この、『都度取りに行く』という部分がまさに『遅延評価』という特徴に値します。
つまり、必要になったら取りに行くという事です。

そして、『必要になったら取りに行く』という特徴のため、
要素数がいくつあるかも不明です。

要素数が分かるためには、目で見て数えれる状態にしなければなりません。
しかしながら、パイプに覆われたままでは不可能です。
つまり、一度ボールを全部取り出して、どこかに保持する作業が必要になります。

そのため、パイプに覆われた状態のIEnumerableでは要素数を把握できません。
ただ、順番に数えるだけです。

イメージ:覆われたパイプの中にボールが詰め込まれている状態

順次値を取り出して評価するため、遅延評価

要素数は分からない

また、yield return という概念もIEnumerableに近いものがあります。
ここでは詳しく説明しませんが、頭の片隅に入れておくといいですよ。

ICollection

ICollectionではどうでしょうか。IEnumerableと比べた大きな違いとして、
ICollectionから要素数を把握できるようになります。
長いパイプに詰めていたボールを、透明な長いパイプへボールを一個ずつ移し替えたのです。

すると、要素数が把握できるようになります。
透明なパイプなので目で見て分かりますね。

ただし、大きく違う点として、メモリに保持するようになるという事です。
透明な長いパイプにボールを移し替える作業が、まさにそれに値します。

と同時に、IEnumerableの代名詞でもあった遅延評価は、ICollectionでは遅延評価ではなくなります。
要素数を把握するために、すべてのボールを透明なパイプに移し替えました。
移し替えたということは、メモリに落とし込んだという事です。

メモリに落とし込んだため(透明なパイプに入れ直したため)、
Add()やClear()、Contains()などのメソッドも自由に使えるようになります。
もちろん、IEnumerableで出来た、順次に値を評価することも可能です。

イメージ:透明な長いパイプに入れ直される

IEnumerableでできること + 追加・削除・Contain()ができる

メモリに落とし込まれるため、即時評価になる

IList

では最後にIListではどうなるのでしょうか。
ICollectionのときは、『透明な長いパイプに入れ込む』という表現を使っていましたが、
IListになると、『電車の車両』に入れ直されます。
車両はデータの数ごとに用意されており、車両の中にデータが格納されます。

また、車両自体も透明なボディで覆われており、ICollectionと同様に値を目で見ることができます。
また、車両同士を好きなように連結できたり、車両をひとつ外したりという事もできます。皆さんが良く使っている、Listに近いですね。

入れ直される容器が変わっても、できる事はICollectionと変わりません。
要素数も連結数を数えれば分かるし、Add()やClear()もできます。

では、IListでしかできないことは何でしょうか?
それは、途中にデータを追加や削除ができるということです。

ICollectionでは、透明なパイプであるため、
途中から割り込んで値を追加するのはできませんでした。
しかしながら、IListではそれが可能になります。削除も同様です。
また、ICollectionの時と同様に、メモリに落とし込まれます。

イメージ:透明な車両に入れ直され、電車のように連結される

ICollectionで出来る事 + 途中でデータ追加・削除ができる

メモリに落とし込まれるため、即時評価になる

IListとListは同じものなのか

いよいよ本題に入ります。
今まででIListまでのコレクション御三家のインターフェースについて解説しました。
ここで皆さんがよくお世話になっている『List』というクラスについて見ていきましょう。

Listというクラスは、IListをそのまま具現化したクラスなのでしょうか。

言いたいことは、(IListがインターフェースであるということは置いといて)
IList = List なのかという事です。

しかしながら、これは違います。

では、IListでできる事についてもう一度まとめてみましょう。
実は、ジェネリックとコレクションの2種類のIListがあるのですが、
ここではまとめて考えて、

  • アイテムの要素番号を返すメソッド
  • 挿入するメソッド
  • 削除するメソッド

の3種類あることにします。
つまり、IListは上記の3種類のメソッドを持っているという事ですね。

ですが、Listになると、

    ・Add()…
    ・BindSearch(..) …
    ・Clear()
    ・ConvertAll(..)
    ・Contains(..)…
    ・CopyTo() …
    ・Exists(..)
    ・Find..(..)…
    ・ForEach(..), GetEnumerator, GetRange(..), IndexOf(..)
    ・..Insert..(..)
    ・Remove..(..)
    ・Sort(..)
    ・Reverse()
    ・ToArray(), TrimExcess(), TrueForAll(..)

圧巻ですね。多すぎます。
IListは3種類に対し、Listは数10種類あります。
このように
IListが実装すべきメソッドの数と、
Listが実際に抱え込んでいるメソッドの種類は同じではありません。

Listの方が圧倒的に多量のメソッドを持っていることを覚えましょう。

スポンサードサーチ

ArrayはICollection?

次にArrayに関して見ていきましょう。
正直これはややこしいです。

まず、Arrayとはもちろん、みなさんが配列と呼んでいる型のことです。
(var array1 = new int[10] とかこんな感じのやつです。)

Array(配列)について軽く復習しますと、
配列を作る際に保持できる引き出しの大きさを決めておいて、引き出しに値を格納することができます。
最初に引き出しの大きさを決めますので、作った後に引き出し自体の追加や削除はできません。
そして、大きなメリットとして、引き出しの番号が分かっていれば、直でアクセス可能です。

では、皆さん考えてみましょう。
Arrayはどんなインターフェースを実装しているのでしょうか。

実は、.net core System.Arrayで見てみると、きっちりとIListを実装しています。
IListを実装しているという事は、追加や削除ができるはずです。

ところが、配列の性質上、大きさが初期化時に決まり、連なった領域に値を入れていくという形式をとっています。
領域を追加したり、削除したりはできません。
そして、このArrayに関しても同様で、追加・削除はできません。

IListを実装しているのに、実際では追加や削除ができないのです。
何か特別な処理がなされているのでしょうか。

.Net Framework4.8 System.Arrayの中身を見てみると分かります。
そちらのリンクに行って、Ctrl+Fで検索窓を開き、『Insert』と入力して見ましょう。

IListによる実装が必要なInsertメソッドはきちんと定義されていますが、
『NotSupportedException』というエラーを返しています。
よくよく見ると、削除系でも同様のエラーを返しています。

つまり、まとめると、
Arrayは、IListのインデックスアクセスとReadOnly系の性質だけ使えるということです。

配列は連続したヒープ領域を確保します。それより外は、他のメモリ領域になります。

もし配列で追加ができてしまったら、
連なった領域のすぐ隣が空いているかどうか保証はありません。
そんな理由で、追加・削除系のメソッドは実装するように見せて、エラーを投げて対処しているようですね。

ですが、逆に配列を使用すれば、値が変わらないという事が保証されます。
また、インデックスアクセスも、リストに比べ早そうですね。

つまり、あるコレクションに対してToList()やToArray()を行うとき、
コレクションは変更せず、ReadOnlyな感じでライトに使いたいときは、ToArray( ) がもってこいです。

その逆で、
要素の追加・削除・ソート系を自由に行いたいのであれば、ToList( ) にしなければなりません。

こんな風にして使い分けましょう。
何でもかんでもToList()すれば無駄が多くなりますので、使いどころを選ぶようにするとプログラマーとしてレベルアップできますよ。

まとめ

今回は、IListを実装している具象クラスである、ListとArrayについて取り上げ、
それぞれどんな時に使うべきかを紹介しました。

  • 要素の追加・削除・ソートをしたい => List
  • ReadOnlyな感じで使いたい => Array

となります。

余談ですが、
ArrayはICollectionレベルの具象クラスなんだろうなと思っていた方もいたのではないでしょうか。

今回でArrayはきちんとIListを実装していること、Arrayは特殊(だいぶ無理してる)なんだということも分かりましたね。