++C++; // 未確認飛行 C ブログ

http://ufcpp.net/

C# 7に向けて(1): 変数やフィールドの書き換え

with 6 comments

プログラムを書くときに、「値が書き変わらないことの保証」(不変性)がほしいことは結構あります。

C# だと、現状(6も含めて)では、const 修飾と、フィールドに対する readonly 修飾くらいしかなくて不便に思うことがありました。

もちろん課題があるからこそのこの現状なんですが、C# 7では、「less ambitious」(野心を抑えて。妥協的)でも、もう少し不変性の保証を増やそうという提案がされています。

readonly は「浅い」不変性

「変数や引数にもreadonlyを付けれるようにしてくれ」という要望は昔からあるものの、これまでやらなかった理由は単純で、そこまで有用でもないから。有用は有用だと思うんですけど、効果が薄いのと、混乱しそうな要素もあります。

問題は、readonlyが浅い(shallow)不変性保証しかできないことです。ここでいう「浅い」って言うのがどういうことか説明するために、以下の例を見てください。B.A には readonly がついているので書き換えできませんが、そのさらに中身の A.X は書き替えられます。

ただし、readonlyがついているフィールドが構造体だとちゃんと中身も書き替えられなくなります。

絵にするとわかりやすいんですが、要するに、readonly がついているまさにその箇所だけ書き換えできなくなりますが、その参照先は普通に書き替えれます。これが「浅い」という理由。

ReadonlyIsShallow

この逆、つまり、参照先も階層的にたどって不変性を保証するのは「深い」(deep)不変性といいます。後述しますが、C# 7提案では、深い不変性には別のキーワード、immutableというものを追加することになりそうです。

で、変数や引数に対して readonly 修飾を認めた場合でも同じく浅い不変性保証しかできません。

ReadonlyVariableIsShallow

この意味での変数・引数の不変性ももちろん欲しいことは多いんですが、「浅い」「深い」の区別がわからないまま誤用してしまう怖さがあります。

また、変数や引数は、「基本的には」(基本じゃないパターンは後述)、かなり短いスコープの間でしか存在しないものなので、多少の便利機能を追加したところで、プログラミング作業全体で見るとどの程度の効果があるのかという問題もあります。

実は広いスコープ

「スコープが狭いものに多少の便利機能を足しても大した効果は…」というのはその通りなんですが、変数や引数のスコープというか、影響範囲を広げてしまうような構文もあったりします。

1つは、ラムダ式の変数キャプチャ。

この通り、ローカルな変数に見えて、関数の外から書き替えれるものが作れます(value という名前のこの変数を参照できる範囲は確かにローカルなんですが、f() を呼び出すたびに値が書き変わるという意味ではメソッドの外に影響範囲が漏れ出ています)。C# のラムダ式では、外部の変数をキャプチャして読み書きする機能があります。この時、結構大げさな仕組みが働いていて、

  • ラムダ式からcompiler-generatedなクラスが作られる
  • キャプチャした変数はそのクラスのフィールドに格上げされる
  • 作ったクラスをnewして、そのインスタンス メソッドの参照を返す

みたいな状態になります。ローカル変数に見えて、実体としてはフィールド。そして、現在のC#の構文では、この「格上げされたフィールド」をreadonlyにすることはできません。

もう1つは、ref 引数。

外からもらった参照先を書き替えれるので、当然、メソッド内だけの問題ではなくなります。

通常は、「ref 引数がそもそもそういうものだ」ということで問題にならない(呼び出し側にも ref を付ける必要があるし、誤用はないはず)んですが。ただ、ref 引数には、「大きめの構造体をコピー渡ししたくない。別にメソッド内で書き替えるつもりはない」という用途もあります。この場合に、「書き替えるつもりはない」という意図を示せないところが問題です。これに対して、readonly ref な引数を作れればこの問題は解消します。

この辺りの問題が、ようやく重い腰を上げて、C# 7で変数・引数のreadonlyを提案する動機のようです。「浅い」「深い」の混乱は、次節のimmutableの導入で緩和。

深い不変性の導入

ということで、以下のような方法で深い不変性を導入。

  • 型に immutable 修飾を付ける。その型のフィールドには以下のような制約が付く
    • フィールドは必ず readonly
    • フィールドの型に使えるのは同じく immutable 修飾がついている型だけ
  • immutable な型は、immutable な型からしか派生できない

これで、「深い」、つまり、参照先も階層的にたどって不変であることが保証できるという仕組み。

ImmutableModifier

新しい修飾キーワードを導入したのは「浅い」「深い」の区別のためです。constもあるので、C# には3つの意味の不変があることに。

  • const (constant): コンパイル時に確定していて、実行時にはリテラルと全く同じ扱いになる。最初から一定。
  • readonly (read-only): コンストラクター内でだけ書き換えできて、以降は不変。浅い(shallow)。
  • immutable: 同じくコンストラクター内でだけ書き換えできて、以降は不変。深い(deep)。

immutable は型に付くものなのもポイント。例えば、前節で出てきた、readonly ref (メソッド内では書き換え可能な参照)みたいに、「元々は書き換え可能なものが、ある文脈下でだけ不変」みたいなことはなくて、immutable なクラスのインスタンスは、どこであろうと、常に不変です。(並列処理をする場合などではこれが結構大事。例えば、readonly ref が付いている引数の場合、他のスレッドが別の場所で書き替えれるので、並列処理すると不変性の保証ができません。)

課題

「7」までおあずけをくらっているくらいですから、結構課題もあります。これから解決する方法があればよし。ない場合どう妥協して、その妥協をC#開発者にどう伝えていくか。あるいは、課題がやっぱり深刻すぎるとなれば、提案自体取り下げとなります。

まずは、面倒くささ。

readonly 変数に関しては、最初の方でも言った通り、変数のスコープなんてたかだかしれているものに対して、いちいち readonly int x とか readonly var x とか書きたいかという話。これに対して、「readonly var」の短縮形として、let もしくは val あたりを使ってはどうかという話は上がっています(前者は F# など、後者は Scala が使っているわけで)。

(var と val で区別とか、日本人的にはほんとやめてほしいですが… 割かし最近、variant (可変量)と valiant (勇猛果敢)を間違えてしまったところだし…)

immutable についても、全フィールドを readonly に作るのって、現状のC#の構文だと結構めんどくさい。いったい何か所に同じ名前を書かせれば気が済むのかと…

これに関しては、C# 7で挙がっている他の提案として、Records っていうのがあって、これが「一番簡素な書き方をしたら immutable になる」という方向で簡素化されそうではあります。

次に、完全性。どこまでちゃんと immutable や、それに付随した概念を保証できるか。

例えば、以下のような問題点は指摘されています。

  • .NET では、リフレクションを使えば private readonly なものすら書き換え可能なので、原理的には不変性を崩せる
  • immutable な型のメソッドは、pure (引数が同じなら結果が常に同じ)であることを期待されるけども、いくつかこれを崩せるものがある
    • immutable でないクラスの静的なフィールドを参照
    • ファイルシステムなど、外の世界を参照
    • new で新しいオブジェクトを作ってしまう(値としては常に同じオブジェクトを構築できるかもしれないけども、参照先が毎度変わる)

逆に、ほんとに readonly しか認めない世界は厳しすぎやしないかという話も。

  • 遅延初期化とか、値のキャッシュとか、「型の外から見ると immutable っぽく見えるけど、内部的にはフィールドの書き換えがある」ものはどう扱うか
    • immutable 修飾を付けるとこういうことは基本、できない
    • 「unsafe immutable」みたいに、「型の実装者が immutable であろうと努力しているけども、コンパイラーの検証はできない」文脈を作るべきか

その他、注意が必要な点もいくつか。

  • sealed でない型については、後から mutable/immutable を変えれない(変えると、利用者側のコードを壊す)

こういった課題は、それこそ90年代のC++の時代から言われていることなんですけども、なかなかいい回答もなく今に至っています。しかし、課題はあっても、それ以上にメリットが大きいだろうということで、おそらくこの辺りは外されることなく(具体的な文法はまだこれからいろいろ変わるとしても)C# 7に入るんじゃないかなぁと思われます。

広告

Written by ufcpp

2015年2月1日 @ 19:00

カテゴリー: C#

Tagged with

コメント / トラックバック6件

Subscribe to comments with RSS.

  1. […] 前回、readonlyローカル変数のところでちょっと話題にも出しましたが、C#のラムダ式で、ラムダ式の外の変数をキャプチャした場合、参照渡しになって(内部的にはクラスとそのフィールド生成とかいう結構大ごとなコード生成が起きます)、いろんなところから値が書き替えられる可能性があります(書き換えを防ぐという観点でいうと、前回のreadonlyローカル変数も有効な解決策ですが)。 […]

  2. […] C# 7に向けて(1): 変数やフィールドの書き換え […]

  3. […] immutable)に作っています(参考:C# 7に向けて(1): 変数やフィールドの書き換え)。「めんどくさいから後で」とか言って mutable […]

  4. […] 「C# 7に向けて(1): 変数やフィールドの書き換え」で説明したように、値が書き変わらない保証がほしいときがあります。もちろん書き替えたい場合もあるわけで、選べるのが望ましいです。 […]

  5. […] C# 7に向けて(1): 変数やフィールドの書き換え […]

  6. […] C# 7に向けて(1): 変数やフィールドの書き換え […]


コメントを残す

以下に詳細を記入するか、アイコンをクリックしてログインしてください。

WordPress.com ロゴ

WordPress.com アカウントを使ってコメントしています。 ログアウト / 変更 )

Twitter 画像

Twitter アカウントを使ってコメントしています。 ログアウト / 変更 )

Facebook の写真

Facebook アカウントを使ってコメントしています。 ログアウト / 変更 )

Google+ フォト

Google+ アカウントを使ってコメントしています。 ログアウト / 変更 )

%s と連携中

%d人のブロガーが「いいね」をつけました。