本サイトからスピンオフした姉妹サイトです。7.0で登場した新しいタプル(ValueTuple構造体)は、複数の値をひとまとめにして扱うのに便利なデータ構造だが、その実装は一般的な構造体のガイドラインに従っていない。なっているのか、技術的背景を追う。7.0のタプル機能では、内部的にValueTuple*1という構造体を使っている。この構造体は型引数の数が0~8個のものがあるが、例えば2引数のものはリスト1のような構造になっている。4.7がインストールされていない環境では、NuGetでSystem.ValueTupleパッケージをプロジェクトに追加する必要がある。詳しくはこちらを参照。C#に慣れ親しんだ人からすると、この構造体に少々違和感があるかもしれない。フィールドがpublicで、書き換え可能(mutable)になっている。ガイドライン的に避けるべきとされてきた構造である。一般には以下のことが推奨されている。(構造体に限らず)単純なデータの読み書きであってもプロパティにするべき構造体は書き換え不能(immutable)に作るべき先にいっておくと、別にガイドラインが変わったわけではなく、一般論としてはいまだこのガイドラインが正しい。C#の進歩によって問題が緩和された面もある。少ないながらガイドラインに沿わない方がよい場面もあり、ValueTuple構造体はまさにその例となる。本稿では、このガイドラインや、ValueTuple構造体がこの構造を採用した背景について説明していく。locals)という機能がある。この機能の導入によって、構造体の書き換えに関する事情が変わった。参照戻り値の導入以前に、書き換え可能な構造体がどういう問題を起こしていたかを見てみよう。書き換え可能な構造体が起こす問題の例この例ではまず、以下のような2つの型が用意されている。問題となるのは、Mainメソッド内でやっているように、Polygonのインデクサーを通してPointのXプロパティとYプロパティを書き換えようとしたときである。連載第7回でも触れたが、プロパティやインデクサーを介して構造体の値を返すとコピーが発生する。上記コードで書き換えようとしているのはコピーであって、元の値は書き換わらない。クラス(参照型)と構造体(値型)の違いをあまりはっきりと把握できていない開発者もいるし、たとえしっかり分かっていてもこの例のようなミス(書き換えたつもりが書き換わっていない)はやりがちである。コピーを書き換えられてしまうことで起きる問題なので、構造体は書き換えられないように作る方が、事故が減ってよいとされる。一方で、参照戻り値を使えば、リスト3に示すように、コピーは作られず、Polygon内で持っているPointを書き換えられる。参照戻り値で構造体を書き換える例もちろんこれは、「書き換え可能な構造体が許容されるようになった」ということであって、「書き換え可能であることを推奨する」というものではない。実際のところ、構造体を書き換え可能(mutable)に作るべきか不能(immutable)に作るべきかは状況次第である。プロパティやインデクサーも、書き換え可能な構造体に対しては参照戻り値を使用し、書き換え不能な構造体に対しては通常のget/setを使用するというように使い分けるべきかもしれない。readonlyちなみに、書き換え可能に作った構造体であっても、readonly修飾子を付けることで書き換え不能にできる。リスト4のように、readonlyなフィールドのメンバーを書き換えようとするとコンパイルエラーになる。書き換え可能な構造体のreadonlyなフィールドもし、構造体を書き換え可能に作ることにしたなら、この仕様も活用するといいだろう。実装の隠ぺいオブジェクト指向的な技法の1つに、「実装の詳細は隠せ」というものがある。publicな部分さえ変えなければ実装方法を変えても利用側に影響が出ないことから、変更に強いプログラムが書ける。リスト5のようなコードを考えてみる。2つの構造体があるが、いずれも「単位円周の点の座標」を表す構造体で、publicな部分(コンストラクターと、X/Yプロパティ)は全く同じである。一方で、内部的には、Circular1ではX/Yそれぞれの値を持っていて、Circular2では角度の値を1つだけ持っている。最初はCircular1のような実装をしていたとしよう。途中で、メモリ消費量の削減が最重要課題になったため、Circular2のような実装への変更が必要になったとする。このとき、publicな部分は変えていないので、すでにこの構造体を使っている箇所があったとしても、何も影響も出さずに構造体の中身を書き換えることができる。いうメリットがあるため、一般論としては、フィールドをpublicにするべきではない。「外から見える挙動」が大事という意味で、オブジェクト指向的な考え方は「振る舞い(behavior)が主役」といわれる。データが主役の設計これに対して、「データそのものが主役」な使い方もあるだろう。いうデータを持っているかということ自体が仕様であって、そこを変更するということは利用側に影響が出てしかるべき場面は普通に多い。実装を隠せといわれても、読み書き両方できるpublicなプロパティしかないような型を書くことも多いだろう。通信やデータの保存が絡む場面ではよくある。「2次元上の点のX座標とY座標を、それぞれdouble値で保存・送受信する」といわれれば、リスト6のようなデータ構造そのものが仕様になるだろう。データ構造そのものが仕様となる例ValueTuple構造体はまさに「データが主役」な用途で使うものである。実装の詳細を隠す意味はあまりない。いう類いの型では、別にフィールドがpublicであってもプロパティと大差ないだろう。一応、クラスであれば、リスト7に示すように、virtualにできることでプロパティの意義が出てくる。プロパティを使う意義がある例しかし構造体の場合は継承できないため、このような利点も得られない。データが主役、かつ、書き換え可能な構造体では、フィールドを直接publicにして起こる不利益はないだろう。メモリ上のレイアウト前節の「どういうデータを持っているか」ということ自体が仕様という話をさらに一歩進めて、「どういうデータをメモリ上にどういうレイアウトで持つか」まで含めて仕様としたい場合がある。高パフォーマンスを求められる分野で必要になる。リスト8のような構造体を考える。これは、8bytesのデータを1byteずつ読み書きする構造体である。8bytesのデータを、1byteずつ読み書きする構造体C#では、こういう書き方をするとA~Hの各フィールドがこの順で、隙間なく並ぶことが保証されている。Vectorのサイズは全体で8bytesになり、最初の1バイト目がAフィールド、その次がBフィールド、......となり、最後の8バイト目がHフィールドである。A~Hがこのレイアウトで並んでいるという前提の上でできる最適化がある。この構造体に対して、メンバーごとの足し算をしたいとする。レイアウトに関する前提がなければ、リスト9のような実装になるだろう。メンバーごとの足し算(レイアウトを前提としない実装)一方で、レイアウトが決まっているなら、リスト10に示すように、ulong(64bit整数)扱いして計算するような実装ができる。CPUではリスト9のコードよりもこちらの方が2~3倍程度高速である。メンバーごとの足し算(レイアウトが決まっている前提の実装)こういう実装で正しく動作するかや、高速化できるかはCPUの種類にもよるため、扱いに注意を要するコードではある。この2~3倍の速度が必要とされる場面は確実にある。そこでレイアウトも含めて固定したいわけだが、レイアウトを決めているのはフィールドの宣言順である。この例の場合はA~Hというフィールドがこの順で並んでいる必要がある。これを「仕様」と考えると、フィールドはpublic(=誰からでも見え、変更は利用側に影響が出る)な方が好ましい可能性すらある。一般的とは言いにくいが、publicなものであってもプロパティよりもフィールドの方が好ましい場合もあり得るわけである。高パフォーマンスを求めるほどこういう場面が増える。連載第6回で説明した通り、C#でも高パフォーマンスを求められる用途が増えているため、フィールドをpublicにすることが多くなるかもしれない。型変換と参照戻り値リスト10の例では、レイアウトの固定を前提として、全く異なる型の間での変換を行っている。いう処理は、これまでであればポインターが必須だった。これも、参照戻り値の登場によって、直接のポインター利用を避けられるようになった。リスト11のような書き方ができる(実行にはSystem.Runtime.CompilerServices.Unsafeパッケージの参照が必要)。参照戻り値を使ってポインターを避ける例Unsafeクラスを使うには、System.Runtime.CompilerServices.Unsafeパッケージをインストールする必要がある。直接ポインターを使っていたものが、間接的利用に変わっただけなので、この書き方で安全になるわけではない。&、*や->などの、煩雑な記法は減り、コードをすっきりさせることができる。3.0の匿名型がある。型名を持たない型という意味では同じだが、用途の違いから、実装に差がある。タプルが内部的に使っているValueTuple構造体にも、類似のTupleクラス(System名前空間)というものが存在する。匿名型もTupleクラスも、書き換え不能(immutable)である。かかわらず、タプル(そしてValueTuple構造体も)は書き換え可能なことを不思議に思うかもしれない。結論からいってしまえば、タプルとValueTupleは構造体(値型)だから書き換えられても問題がなく、匿名型とTupleはクラス(参照型)だから書き換えが問題となる。前述の参照戻り値の話では値型だとコピーが発生することで問題が生じていたが、逆にコピーされるからこそ書き換えても問題ないという場面も存在するのである。具体的にいうと、中身の値を見て等値比較するようなクラスは書き換え不能でなければならない。リスト12のようなクラスがそうである。EqualsメソッドとGetHashCodeメソッドを、プロパティXとYの値を見て等値判定するようにオーバーロードしている*1。ちなみに、起こりうる問題を説明するために、あえて書き換えできるように作ってある。通常であれば、参照型のインスタンスはどこを参照しているかによって等値判定を行う。中身が同じであっても、同じ場所を参照していなければ等しいとは見なされない。参照型であるため、このクラスのインスタンスは複数の場所で共有される場合がある。その結果、どこか自分が把握していない場所で他者に書き換えられることがある。参照型のこの性質は、中身による等値比較と相性が悪い。「等しいと思っていたものが、気が付けば等しくなくなっていた」ということがあり得るわけだが、これが大きな問題となる。この問題として特に有名なのは、ハッシュテーブルのキーとして使う場合である。リスト13の例を見てほしい(Dictionaryクラスの内部実装はハッシュテーブルである)。リスト12のMutableClassクラスをキーにしていることで、テーブル内の検索や削除が不可能な状況に陥る。書き換え可能なクラスをハッシュテーブルのキーとして使う例ハッシュテーブルは名前通り、キーのハッシュ値によって値の格納場所を決定している。}から計算したハッシュ値を使っている。}が入っている状態に陥る。}のハッシュ値を使ってたどりついても、肝心の中身がEqualsでは一致しないため、検索不能になる。一方で、キーが構造体であれば、この問題は起きない。リスト14のコードは、クラスから構造体に変更した以外はリスト13のコードと同等だが、問題は解消されている。コピーが起きることによって、書き換えてもハッシュテーブルに格納したインスタンスには影響を及ぼさないからである。参照型である匿名型やTupleクラスは書き換えられてはまずいが、値型であるタプル、ValueTuple構造体は書き換え可能でも問題ない。タプルの用途これまでの説明はおおむね、タプルが「書き換え可能でもよい」「フィールドをpublicにしてもよい」という話である。実際にそうすべきかどうかはタプルの用途を見て考えるべきだろう。タプルは複数の変数、フィールドや戻り値などを束ねる用途に使われる。そのため、タプルの要素はそれら変数などと同様の書き心地であることが好ましいだろう。その結果が、書き換え可能で、フィールドがpublicな構造である。タプルは、図1に示すように変数や引数と、図2に示すようにフィールドと、「t.」というような接頭辞が増える以外は同じ書き方ができる。7.0で参照戻り値などの機能が入ったことで参照(refやout)を使う機会は増えると思われる。タプルの要素を参照として渡すにはプロパティではなく、フィールドでなければならない。図2の例ではフィールドを直接持たず、タプルとしてまとめているが、これは厳密な型付けと緩い型付けの橋渡しに使える。リスト15~リスト17では、同じ「int型の値XとYを持つ型」だが、書き換えの可否、値型か参照型か、単に値を持つかさらに追加機能を持つかなどの用途の差で別の型となっている。int型の値XとYを持つ変更通知付きのクラスリスト15は書き換え不能な構造体、リスト16は書き換え可能で単なる値を持つだけのクラス、リスト17は変更通知付きのクラスである。これらのクラスは、Valueというタプル型のフィールドを介して相互にデータの受け渡しができる。タプルを書き換え不能にしたければ、リスト15の例のようにreadonly修飾子を付ければよい。まとめValueTuple構造体が書き換え可能で、publicなフィールドを持つ背景について、以下のような話をしてきた。高パフォーマンスを求める場面なので、フィールドの並び自体に意味がある場合がある等値比較などの面では、構造体でこそ書き換え可能で構わない(クラスの場合に書き換えがまずい)場面もある一見すると親しまれたガイドラインに反しているようでも、必要があってのことである。ガイドラインをうのみにするのではなく、ガイドラインがある理由や、時代による変化、ガイドラインに当てはめられない例をきっちり把握し、状況に応じた使い分けをしていきたい。1.0がプレビュー版だった頃からC#によるプログラミング入門を公開していて、C#とともに今年で15年になる。最近の自己紹介は「C#でググれ」。※以下では、本稿の前後を合わせて5回分(第13回~第17回)のみ表示しています。INDEX]を参照してください。C#への「インターフェースのデフォルト実装」の導入(前編)C#におけるインターフェースとは、ある型が持つべきメソッドを示す「契約」であり、実装は持てない。このことが大きな問題となりつつある。今回から全3回に分けて、C#がこの問題にどう対処しようとしているかを見ていく。C#への「インターフェースのデフォルト実装」の導入(中編)前回は一般論としてのインターフェースとその課題を見た。今回はC#にインターフェースのデフォルト実装を導入すると、どのようなコードが書けるようになるのか、導入するために必要な修正点などについて見ていく。C#への「インターフェースのデフォルト実装」の導入(後編)破壊的な影響を他に及ぼすことなくインターフェースの機能を拡張するには、デフォルト実装に加えて拡張メソッドも使用できる。今回はこれら2つの方法がなぜ必要なのか、それぞれが得意としている分野について詳しく見る。7.0で登場した新しいタプル(ValueTuple構造体)は、複数の値をひとまとめにして扱うのに便利なデータ構造だが、その実装は一般的な構造体のガイドラインに従っていない。なっているのか、技術的背景を追う。7.1がリリースされた。7.2/7.3に含まれる予定の機能、そこから見えてくるものについて考えてみよう。機械学習エンジニアを目指す人~初中級者のための技術情報サイトです。「機械学習&AI」「クラウド&ビッグデータ」「IoTデバイス」のネット記事情報を共有中。開発者のためのSlackチーム、作りました:「機械学習&AI」「クラウド&ビッグデータ」「IoTデバイス」のネット記事情報を共有中。2020最適なビューへの切り替えを推奨します:スマートフォン用表示に。