Archive for 2月 2015
Roslyn 2/28
割と「週刊」化している、 https://github.com/dotnet/roslyn 内の今週の動き。
Milestone: C# 7
Milestone が「C# 7 and VB 15」なものに2項目追加。
プロパティ内限定(get と set の両方で使いたいけども、その他のメンバーからは触られたくない)スコープが欲しいという話。
C# 2.0 とか 3.0 の頃から欲しいとは言われていたものの、全然入らなかった機能。当時は自動プロパティ(3.0 で入った T X { get; set; } だけでプロパティが作れるやつ)だけでも、Anders (.NET のものすごく偉い人)がなかなか Go サインを出さなかったらしいですし、時代もだいぶ変わった感が。ここ数年、Anders はご意見番的な立ち位置で、Mads (C# チームのPM)の統括に代替わりできているみたいで。
仮想メソッドを派生クラスでオーバーライドする際に、戻り値に共変性を認めたいという話。例えば、class Derived : Base {} があるとき、ある規定クラスが virtual Base X() メソッドを持っていたとき、こいつの派生クラスは override Derived X() を定義したい。Java には昔からあって C# でもたまに欲しくなる話ではあります。
実装的には、.NET ランタイム(IL 仕様)の変更は不要そう。派生側に、override Base X() (基底クラスのやつのオーバーライド)と、new virtual Derived X() (別の仮装メソッドを追加)の両方を作って、override 側から new 側を呼び出すようなコードを生成すればいいはず(そういうやり方は、現在の C# では書けないものの、IL 仕様的にはできる)。
決定論的なモジュール ID
現状の Roslyn C#/VB コンパイラーは、コンパイルのたびに生成結果のモジュール(exe や dll)のバイナリが変わってしまうそうです。これは、ポータビリティの観点からあまりよろしくないので、決定論的な生成結果を得るための調査を始めた模様。
一番の原因は、モジュールの ID にタイムスタンプが入っているかららしいです。これを、モジュールのハッシュ値みたいなものに変えたいというのがまず第一。ただし、これが決定論的であるためには、タイムスタンプに実際のコンパイル時刻などは使えなくなります。ちゃんとした時刻が入っていることを期待したプログラムの動作保証がなくなります。
そして、ハッシュ値が決定論的であるために、コンパイラーが生成するモジュール自体が決定論的である必要があって、そのためにいくつか別の課題が上がっています。
- String heap in generated IL should be deterministic #360
- 文字列リテラルの出力順が決定論的でないとダメ
- EditAndContinueTests.AnonymousTypes changes emit order in 64-bit runner #223
- 匿名型の出力順が決定論的でないとダメ
- Produce PDBs with deterministic GUIDs #926
- PDB (Program Database: デバッグ用のシンボル情報とかが入ったファイル)の GUID も決定論的でないとダメ
- デバッグ実行時に PDB を参照するため、デバッグ ビルドではモジュールが PDB ファイルの GUID を参照している
- PDB format and caller file name arguments include full path names in compiler output #949
- ソースコードのパスがフルパスで入っているので、今のままだとビルドしたマシンごとに出力結果が変わる
- 相対パスか何かに変えないといけないし、そのために(相対パスの起点となる)ソースファイル/ディレクトリをコンパイラーに渡すオプションが必要
VS 2015 CTP 6、Microsoft.CodeAnalysis.Analyzers RC 1
昨日、Visual Studio 2015 の CTP 6 が出たみたいですが。
まだRC(リリース候補)じゃなくてCTP(テクニカル プレビュー)なんですねぇ。
一方で、Microsoft.CodeAnalysis.Analyzers (Roslyn の NuGet パッケージ配布)は RC1 になりました。Visual Studio 2015 CTP 6 の Roslyn (要するに、CTP 6 が使っている C# コンパイラー)はこの RC 1 のやつだそうです。
つまり、C# 6.0 は RC 1 になりました。
CTP 6 での C# 6.0 の更新点
バグフィックスとかは置いておいて、C# 6.0 の文法的な話でいうと、2点更新があります。
- Exception Filters の仕様変更
- IFormattable な String Interpolation
Exception Filters の仕様変更
例外フィルターで、if ではなく、when を使うようになりました(新しい文脈キーワード。catch の条件句の直後でだけキーワードになる)。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
try | |
{ | |
} | |
catch (ArgumentException e) when (e.ParamName == "x") // 前まで if だった | |
{ | |
// パラメーター名が x の時だけはエラー無視 | |
} |
if だとね… 波かっこ {} の有無で意味が変わっちゃうとかいう恐ろしい仕様だったので。これは仕方がない。
IFormattable な String Interpolation
「C# 6.0 正式版までには入る」機能のうちの最後の1つ、FormatProvider 指定できるバージョンの文字列補間が実装されました。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
private static readonly IEnumerable<string> Cultures = new[] { "en-us", "fr", "zh-hk", "ja-jp" }; | |
public static void Y() | |
{ | |
var x = 10; | |
IFormattable f = $"{x :c}, {x :n}"; | |
foreach (var c in Cultures) | |
{ | |
var s = WithCulture(f, c); | |
Console.WriteLine(s); | |
} | |
} | |
public static void SameAsY() | |
{ | |
var x = 10; | |
IFormattable f = System.Runtime.CompilerServices.FormattableStringFactory.Create("{0:c}, {1:n}", x, x); | |
foreach (var c in Cultures) | |
{ | |
var s = WithCulture(f, c); | |
Console.WriteLine(s); | |
} | |
} | |
private static string WithCulture(IFormattable f, string cultureName) | |
{ | |
return f.ToString(null, System.Globalization.CultureInfo.CreateSpecificCulture(cultureName)); | |
} |
これが、C# 6.0 の新機能の中で唯一、.NET Framework のバージョンアップする(もしくは、同シグネチャのクラスを自前で追加実装してやる)必要のある機能。上記コード例は、ターゲットを .NET Framework 4.6 にするか、FormattableStringFactory/FormattableString クラスを自作しないと動きません。
ちなみに、FormattableString は、当初、構造体にするかもという話も出ていましたが、結局クラスになったようです。あと、$”” の展開結果は、直接 new FormattableString(format, args) するんじゃなくて、ファクトリ メソッド FormattableStringFactory.Create(format, args) を介して作られるようになったみたいです。後からの拡張ができるようにしてあるのかも。
Roslyn 2/21
https://github.com/dotnet/roslyn 内の動きから、今週もいくつかネタを。
バージョン違いでコンパイル
そういや先日のネタの為に、複数バージョンのC#コンパイラーを同時に読んで、結果を色分けして表示するPowerShellスクリプトを書いたわけですが、その後も度々活躍していたり。Roslynのリポジトリ見てると結構、古いバージョンだとどうだったんだっけ?とか気になったり。
ということで、このスクリプトもGistにでも置いておきます。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
cls | |
. C:\Windows\Microsoft.NET\Framework64\v2.0.50727\csc.exe .\a.cs | Write-Host -ForegroundColor DarkCyan | |
. C:\Windows\Microsoft.NET\Framework64\v3.5\csc.exe .\a.cs | Write-Host -ForegroundColor DarkMagenta | |
. C:\Windows\Microsoft.NET\Framework64\v4.0.30319\csc.exe .\a.cs | Write-Host -ForegroundColor DarkGreen | |
. 'C:\Program Files (x86)\MSBuild\12.0\Bin\csc.exe' .\a.cs | Write-Host -ForegroundColor DarkRed | |
. 'C:\Program Files (x86)\MSBuild\14.0\Bin\csc.exe' .\a.cs | Write-Host -ForegroundColor DarkBlue |
gitter運用始まりました
Miguel(Monoの偉い人)がgitter立ててた。
https://gitter.im/dotnet/roslyn
gitterについて紹介しておくと、GitHubと連携して、リポジトリや組織単位の部屋を立てれるチャットサービス。発言はGitHub互換のmarkdownで書けるし、#123とか書くとGitHubの該当issueへのリンクになるし、:+1:とかでの絵文字も使えるみたい。
Portable PDB
Portable PDB (program database)の仕様が公開されたみたい。
https://github.com/dotnet/roslyn/blob/portable-pdb/docs/specs/PortablePdb-Metadata.md
PDBは、デバッグとかビルド用にプロジェクト内のソースコードの状態が色々記録されてるファイル。例えば、スタックトレースとかブレイクポイント用に実行ファイルのどこがどのソースコードの何行目かとかの情報が入っていたり。あと、インクリメンタルビルドのためとかにも使っているみたいです。
これまでは、一応PDB読み込み用のライブラリは公開されてたりはするものの、詳しいドキュメントはいっさいなかったみたいです。
この手のデータは、このMSが使ってるPDB以外でも、大抵のコンパイラーがそれぞれ独自の形式を持ってたりします。ほんとバラバラ。C#でも、MS製コンパイラーはPDBを使っていますが、MonoのmcsコンパイラーはMDBっていう別形式を持っています。
今回、オープンになった形式(Portable PDB)は、.NETのメタデータの上位互換なフォーマットみたい(物理フォーマットは同じで、メタデータと同じ実行ファイルに混ぜることも可能。要は論理スキーマの追加)。
まあ、デバッグに必要な情報はかなりの部分メタデータと被るので。世の中には「C++でもリフレクションできるよ?PDB読めば」とかいう猛者もいらっしゃるくらいで。ちなみに、「PDBってVC++専用でしょ?」って聞くと、「もちろんコンパイラーごとに別実装する」との返事が返って来たり。
などという深い闇を産んできたわけですが、オープン化で状況改善しそうでほんとになにより。
default(StaticType)
静的クラスがらみのバグフィックスと、それの結果、一部旧コンパイラーから破壊的変更になったという話が1件ドキュメント化。
https://github.com/dotnet/roslyn/blob/master/docs/compilers/CSharp/Static%20Type%20Constraints.md
C#の仕様的に、静的クラス(static修飾を付けたクラス)は絶対にインスタンス化できないわけで、静的クラスの変数はそもそも作れなくしてあります(コンパイルエラー)。
静的クラスAがあったとき、
A x;
はエラーになります。これくらい素直な場合はいいんですが、めんどくさいケースがいくつかあります。まずは、varでの変数型推論。
var x = default(A)
そもそも、default(A)を認めるなよって話ではあるんですが、旧コンパイラーでは default(A) を書けちゃうそうです。Roslyn新コンパイラーでも、互換性のため、default(A)自体は書けるようにした(バグだけど直さない)とのこと。
そして、そのせいで、先ほどのvar x = default(A); が問題に。別途、「型推論の結果、varが静的クラスになるのは認めない」というルールを設定。
次にはまったのは、これをジェネリックなメソッドに渡す場合。void M<T>(T x)がある時の、
M(default(A));
これも別途、推論結果で型パラメーターが静的クラスになったときをエラーにしないといけない(最初、コンパイルエラーになってなくて、バグ報告されてた)。
そして、これとは別に、旧コンパイラーにはもう一個バグがあって、
((dynamic)3).M<StaticType>()
これがコンパイル通るそうです。dynamicが絡むとチェックが甘く。Roslynではこれは直したそうで、破壊的変更になってしまっているとか。まあ、実行時エラーになって他ものがコンパイルエラーになるだけですけども。
Roslyn内のコードをいくつかC#6化
試しにアナライザーのいくつかをC# 6を使って書き換えてみたそうです。
https://github.com/dotnet/roslyn/pull/740
目的は「少しやってみて、皆の印象を聞きたい」みたいな感じみたい。なのでごく一部だけ(Roslynのアナライザーを使えば機械的に全置換とかもできるけど、それはやってない)みたい。
Roslyn コンパイラー仕様
まだ pull-req 通ってないんですが、Roslyn リポジトリ上にこんなものが。
https://github.com/dotnet/roslyn/pull/536
コンパイラー仕様のドキュメント化、始めました。
“コンパイラー仕様”
コンパイラーを作っていると、標準化されている言語仕様では不十分な仕様ってものがどうしても出てきます。C# コンパイラーにもいくつかそういうものがあるんですが、「オープン化したんだからそういう隠れ仕様もドキュメント化しないとダメだよね」という感じで、その手始めに、コンパイラーチームの内部 OneNote で書かれていた仕様を .md 化してリポジトリに追加しようとしているみたい。pull-req が通った暁には、/docs/compilers フォルダー以下にこういう “コンパイラー仕様” が並びます。
具体的にどういうものをドキュメント化しようとしているかというと、
- コマンドライン スイッチとその意味
- 以前のバージョンのコンパイラーからの破壊的変更
- 標準仕様に反するコンパイラーの内部挙動
- 言語仕様に記述されていないコンパイラー機能
- COM 関連や、マイクロソフト特化の動作
- コンパイラーの挙動に影響を与える “well-known” な属性(Obsolete とか Conditional のこと)
- “ruleset” (FxCop 的なものの、どの警告を出してどの警告は抑止するみたいなルール)のファイル構文(syntax)と意味論(semantics)
- C#とVB間の相互運用のための機能
- 名前付きインデクサー(インデックス付きプロパティ)のC#からの利用
- 言語仕様で明確でなく、発散の余地のあるコンパイラー挙動
- 制限次項(例えば識別子長など)
- バージョンごとの変更履歴
など。
そんなのよく気づいたな
今回公開されたコンパイラー仕様のうちの1つ、「Definite Assignment」がネタとして面白かったんで紹介。要するに、「変数には確実に値を与えておけ」仕様に関する話。
C#では、未初期化のローカル変数の参照を認めていません。これ自体は言語仕様にも書かれていることです。問題は、「何をもって未初期化・初期化済みを判定するか」という部分。判定ロジックに関する詳細が言語仕様に足りていないので、「明確でなく、発散の余地のある挙動」になっているみたいです。
例えば、このページで紹介されている例は以下のようなもの。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class Program | |
{ | |
static void Main(string[] args) | |
{ | |
int x; | |
if (false && x == 3) // 恒偽定数式の後ろの、ショートサーキット演算に続く式は絶対に到達できない | |
{ | |
x = x + 1; | |
} | |
} | |
} |
C#コンパイラーは、到達不能な場所では「未初期化変数を使った」判定をしないみたいです。つまり、 if (false) { ここ } とか、if (true) {} else { ここ } とかでは、別に未初期化変数を使ってもコンパイルエラーにならない。
で、コンパイラー挙動がぶれているのは、さらに、ショートサーキット演算(&& とか || とか、左辺の時点で結果が確定したら右辺を実装しないもの)と組み合わさった場合。false && (ここ) とか true || (ここ) とかは絶対に通らないわけで、前述の if の例に習うなら、別に「未初期化変数を使った」判定をさぼってもいいはず。
ところが…
C# 3.0~5.0 まではエラーになるんですって、これ。C# 2.0の頃はエラーになってなかった。そして、Roslynでもエラーにならなくした。というか、明確なルールを加えた。とのこと。
言われるまで気づかなかった… まあ、こんなコード書かないし、誰も気づかないから言語仕様に漏れるんですが。
なんなんですかね、これ。パターン マッチングとか宣言式とかを実装する過程でこれに類するコードが生成されちゃって困ったとかがあったんですかねぇ。それとも、誰かC# 2.0の頃にこの類のコードを書いてる人がいて、C# 3.0の時に「破壊的変更」を不具合報告入れたとか?
Roslyn issues 2015/2/16
前回でひと段落したC# 7提案関連の話、一応リンクまとめておきますか。
- C# Design Notes / デザイン プロセスについて
- C# Design Notes / テーマ
- C# Design Notes 1/28版
- C# 7に向けて(1): 変数やフィールドの書き換え
- C# 7に向けて(2): 性能と信頼性
- C# 7に向けて(3): その他
- C# 7に向けて(4): C# 6に漏れた分
- C# 7に向けて(5): Record types
- C# 7に向けて(6): Pattern matching
- C# 7に向けて(7): まだ具体案の見えていないもの
- C# 7に向けて(8): Tuples
- C# Design Notes 2/4版
- C# 7に向けて(9): Method Contracts
そして今後は週1程度で個人的に興味持ったものをピックアップしていこうかなぁという感じなわけですが。
とりあえず、第1回。
Non-nullable references: a few difficulties and possible solutions
https://github.com/dotnet/roslyn/issues/227#issuecomment-73928184
非null許容な参照型に関する issue ページに、Mads (C# チームの偉い人)が結構まとまった内容のコメントを付けてた。
要点は
- フィールドだと他の場所で書き替えられる可能性があるから、非null保証しにくい
- 参照型 T を、非null参照型 T! の変数で受けてから使う、みたいな処理が必要
- パターンマッチングを使えば、x is T! y みたいな書き方はできる
- 規定値どうしよう
- 参照型の規定値は null なんで、既定でない値の代入が必須
- 特に配列とかout 引数の場合に心配
- ライブラリの互換性
- 非null参照型が入ったら、既存のライブラリをぜひともこれに対応させたいって思う人がかなり多いはずだけども、既存の参照型 T を非null参照型 T! に書き替えると破壊的変更になる
- コードを壊さないようにするためには T から T! への暗黙的変換が必要。でも、暗黙的にしてしまうと、せっかくの非null型の魅力半減
という感じ。
Avoid unnecessary boxing with String.Concat
https://github.com/dotnet/roslyn/pull/415
C# で、”int ” + 1 みたいな書き方(文字列 + その他の何か)をすると、string.Concat 呼び出しにコンパイルされます。この例で言うと、string.Concat(“int”, 1)。
で、string.Concat の引数は params object[] です。整数とか、値型に対して使うとボックス化が発生。
で、この無駄なボックス化を避けるために、値型だった場合、先に ToString メソッドを呼んでしまおうという提案がされています。もちろん挙動変更なので慎重に検討。
とりあえず、この挙動変更で問題が出るようなプログラムはなさそうには思います。また、C# の仕様書を読み合わせてみても、問題のある(仕様違反になる)変更ではないはず。
What language proposals would benefit from CLR changes?
https://github.com/dotnet/roslyn/issues/420
C# 6までは、とりあえずコンパイラーの修正だけで実現できる機能ばっかりでした。
C# 7の提案に入ってからは、まずは要望だけ洗いだしている状態で、コンパイラーだけでやるべきか、.NETランタイムに手を入れるべきかも含めて検討中。
で、今週、「.NETランタイムにも手を入れるならこういうこともできるよ。他に何か要望ある?」ページが建ちました。
Improve memory managment in async methods
非同期メソッドやイテレーター中でローカル変数を定義すると、コンパイル結果的にはクラス生成されて、ローカル変数だったものが実はフィールドに格上げされます。これは、awaitやyield returnを超えて同じ変数を使うためには必要な処置です。
問題は、awaitを超えて変数を使わない場合。awaitを超えないということは、別にフィールドにしなくても挙動は変わりません。ところが、現状のC#コンパイラーは、awaitを超えない場合でも変数をフィールドの格上げします。理由は、デバッグ時、非同期メソッド内に break point を仕掛けて止めた場合、awaitより前の変数の状態を見たいときがあるから。つまり、デバッグのためだけに、無駄な処理を行っています(ローカル変数のフィールド化は、メモリ管理上、結構な無駄)。
この挙動変える?break point で止めた時に await より前の変数覗きたい?デバッグ ビルドの時だけフィールドに格上げすべき?みたいな内容の issue ページ。
C# 7に向けて(9): Method Contracts
今 issue ページができてて大きめのものはこれが最後のはず。Method Contracts。メソッドに対する「契約」。
結構長かった… 結局、全9回に。今後は、「週刊ブログ」程度でよくなるはず。きっと。
契約プログラミング
そもそも「contracts」とは何か的な話は、昔書いたスライド貼って済まそう。
(補足: このスライドでは非null制約を例に挙げて説明していますが、非null制約は契約でやるよりも、「非null参照型」を作る方がいいと思います。C# 7向けの提案の中にはこの「非null参照型」も含まれています。)
一応、.NET 4の頃から、ライブラリとツールでのサポートはありました。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public int Insert(T item, int index) | |
{ | |
Contract.Requires(index >= 0 && index <= Count); | |
Contract.Ensures(Contract.Result<int>() >= 0 && Contract.Result<int>() < Count); | |
return InsertCore(item, index); | |
} |
また、研究的な位置づけのプロジェクトでは、C# に契約プログラミング機能を足したプログラミング言語(Spec#)もありました。
まあでも、C# が言語機能レベルでサポートしないと流行らないよね、きっと。という状態。
問題
.NET 4で契約がらみのクラス(Contract)が追加されてからも、実際のところ、このContractクラスを使ってくれたライブラリは少ないです。
どういう問題があるかというと例えば、
- Contract. を付けないと行けなくていちいちめんどくさい
- 契約の宣言場所がメソッド本体の中(本来、外に見せない内部実装にあたる場所)にある
- 契約は外から見えるべき
- ポスト プロセスが必要
- Contract.Requires/Ensures を書いている場所では実際には何も起こらず、コンパイル結果のILをさらに書き替えてチェックを行う仕組みになってる
- ドキュメントへの反映(「このメソッドはこういう契約を持っている」みたいなのを doc コメントに反映)もポスト プロセス
- ソースコードの静的解析が結構重い
- Visual Studio の上位エディション(確か、当初、Ultimate 限定?Premium でも行けたかも)が必要
- ポスト プロセス、静的解析ともに
提案: C# 言語機能での契約
文法的には Spec# の頃とあんまり変わらないものになりそう。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public int Insert(T item, int index) | |
requires index >= 0 && index <= Count | |
ensures return >= 0 && return < Count | |
{ | |
return InsertCore(item, index); | |
} |
requires (事前条件: 引数が満たすべき条件) や ensures (事後条件: 戻り値が見たすべき条件)の違反は fail-fast (エラー ログを残して即座にプログラム終了)にするようです。契約は、理想を言えばコンパイラーによって違反は全部コンパイル エラーにしてしまう方がいいものです(単に、技術的困難から実行時に残るだけ)。テストによって契約違反は取りきるべき(ちゃんと、呼び出し元がすべて責任をもって引数チェックしてからメソッドを呼ぶ)もの。なので、規定動作は例外すら出さず、強制終了。
一応、requires/ensures の後ろに else throw を付けて、この挙動を変える(例外を投げるだけにする)機能は検討されているようです。
あと、呼び出し元側で責任をもってチェックする必要があるということは、requires/ensures 句内では、メソッドよりもアクセス制限の強いメンバーの参照は禁止されるべきです。internal なメソッドの契約で private フィールドを参照したりはできません。
C# Design Notes 2/4版
新しいC#デザイン ミーティング議事録。
今回の議題は3つ。大きいのは3つ目の Classes with values です。タプルと関連してのレコード再検討。
あっ、あと、おまけで。Roslyn の RC1 を NuGet 公開したそうです。
Internal implementation only
前にC# 7シリーズその(6)で触れた、 ImternalImplementationOnly 属性に関して、もう少し詳細な検討。
問題点に関しても前の issue ページより少し詳細に。そして、今でもできること、コンパイラーに機能追加すればできること、.NET ランタイムにも手を入れるならみたいな話と、それでどのくらい「穴」が改善するかという話がかかれています。
問題はインターフェイスに後からメソッドを足しにくい(一般にもやっては行けないこととされているし、BCL チームのポリシー的には完全にNG)ことなので、こういう属性を足す以外にも、mixins/traits って呼ばれるような言語機能の追加でも問題は回避できるかもしれません。
実際に C# 6 や 7 で、どこまでやるかや、開発ポリシーをどうしていくかなども含めて、BCL チームとの対話も必要そう。
Tuples, resords, deconstruction
その(8)で書いた通り、C#7 に向けて、タプル型も提案されたわけですが。タプル型が入ることによって、レコード型やパターン マッチングについても再検討が必要という話。
特に、タプルとレコードについて、これらの機能は一元化する方がいいかもしれないし、少なくとも統合的にデザインしていく必要があるでしょう。タプルは「レコードの特別な場合」であるべきか、もしかしたらレコードは単なる「型名のあるタプル」であるべきかなどということも考えられます。
レコードの導入動機は、値セマンティクスや immutability を持つクラスを楽に作れるようにするものなので、この辺りを次節で検討します。
Classes with values
今回出たアイディアは以下のようなもの。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Point の実際の値を持つ構造体。 | |
// 現状の C# だと自分で実際にこういう型定義を書く必要があるけども、タプルにできるのでは。 | |
struct PointValue | |
{ | |
public int X; | |
public int Y; | |
} | |
// 値セマンティクスを持つ immutable クラス | |
// レコード型の実装はこうあるべきだろうという内容。 | |
// ここでいう値セマンティクス(value semantics)は、全メンバー比較で Equals、全メンバーのハッシュ値から GetHashCode 計算できるもののこと。 | |
class Point | |
{ | |
// 唯一の構造体メンバーに readonly を付けているので immutable。 | |
// タプルが入るのであれば、public readonly (int X, int Y) Value; でいいはず。 | |
// Point p に対して (var x, var y) = p; で、(var x, var y) = p.Value; の意味に解釈するのもよさそう。 | |
public readonly PointValue Value; | |
// 外から mutable な型で値をもらう。いわゆる「ビルダー パターン」。 | |
// PointValue をタプルにするのなら、スプラッティングが効いて、new Point(x, y) とも書けるはず。 | |
// 簡易構文として、new Point { X = 1, Y = 2 } で、new Point(new PointValue { X = 1, Y = 2 }) の意味に解釈するのもよさそう。 | |
public Point(PointValue value) { Value = value; } | |
// Value に処理を丸投げ | |
public int X => Value.X; | |
public int Y => Value.Y; | |
public override int GetHashCode() => Value.GetHashCode(); | |
public override bool Equals(object obj) => (obj as Point)?.Value.Equals(Value) ?? false; | |
// 値書き換え(immutable なので別のインスタンスを作る)用の "wither" (GetX, SetX みたいなのを getter/setter というように、WithX をこう言う) | |
// p.Value { X = 1 } みたいに、new するとき以外でもオブジェクト初期化子を使えると、wither 書くのが楽になるはず。 | |
// Point WithX(int x) => new Point(Value { X = x }); | |
// あと、var q = p { X = 1 }; みたいなので new Point(Value { X = 1 }) の意味に解釈するのもよさそう。 | |
// これなら、自前の wither 定義要らない。 | |
public Point WithX(int x) { var v = Value; v.X = x; return new Point(v); } | |
public Point WithY(int y) { var v = Value; v.Y = y; return new Point(v); } | |
} |
要点は
- 値セマンティクスを持つクラスは、文字通り Value 1つだけを持つ
- Value 用の構造体を作って、それを受け取るコンストラクターを作ることで、いわゆる「ビルダー パターン」になる
- Value はタプル型を使って定義するとよさそう
- オブジェクト初期化子の拡張で、「wither」も簡単に書けそう
これなら確かにタプルとレコードの親和性がよく、使う側のわかりやすさ的にもコンパイラーの実装負担的にも都合がよさそう。
まあ、これはこれで、元々のレコード型の提案にあった、部分的なメンバーの置き替え(以下のようなもの)の実装が難しくなりそう。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class Person(int Id, string Name) | |
{ | |
public string Name { get; set; } // Name だけ mutable | |
} |
レコードもタプルも、まだ提案の初期段階でこれから変わっていくでしょうし今後どうなるかはわかりませんが、これはこれで面白い検討内容でした。
C# 7に向けて(8): Tuples
C# 7 Proposals な内容、あと Method Contracts だけになったと一息ついたところで、新しい Issue ページが立つわけですが。
タプル、つまり、型名を持たない構造化データ/データの一時的なグループ化の話。
これは結構気合の入った提案文章になっているので、全訳気味に紹介。
System.Tuple のおかげでタプルという言葉だけではあまり期待感が持てないかもしれませんが、C# 開発者的にインパクトのある言葉で表現すると「アセンブリをまたげる匿名型」「メソッド引数・戻り値やフィールドに使える匿名型」です。
背景
「複数の値の一時的にグループ化」が必要な最もよくある例は、メソッドの引数リストでしょう。C# をはじめ、多くのプログラミング言語がこれをサポートしています。要するに、複数の引数を持つメソッドには、F(1, “abc”) というように複数の値を渡すわけですが、ここで、(1, “abc”) というような値のグループ化が行われていると考えられます。
この次に値のグループ化を必要とするのは、引数の逆、つまり戻り値です。この需要、「多値戻り値」を楽に書けるプログラミング言語はとたんに少なくなります。最近では多値戻り値が使えるプログラミング言語が増えてきていますが、C# もC# 7でその流れを追従することになりそうです。
まず、C# の現状がどうかというと…
out 引数
戻り値を複数返したい場合の1つ目の選択肢は out 引数です。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public void Tally(IEnumerable<int> values, out int sum, out int count) { … } | |
int s, c; | |
Tally(myValues, out s, out c); | |
Console.WriteLine($"Sum: {s}, count: {c}"); |
利点
- 複数の戻り値それぞれがちゃんと名前を持つ
- しっかりした命名は下手なコメントや仕様書よりもよっぽど重要
- どの値が何を意味するかわかりやすくなる
欠点
- 非同期メソッドの戻り値に使えない
- 使う側で、事前に変数の準備が必要
- 無駄に行数が増える(複数のステートメントが必要)
- var が使えない
System.Tuple
「無名の一時的な値のグループ化」を意味する型として、 .NET 4 で Tuple クラスが追加されました。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public Tuple<int, int> Tally(IEnumerable<int> values) { … } | |
var t = Tally(myValues); | |
Console.WriteLine($"Sum: {t.Item1}, count: {t.Item2}"); |
利点
- 非同期メソッドの戻り値にもできる(Task<Tuple<T1, T2, …>>)
欠点
- 名前が消える(Item1, Item2, … という意味のない名前になる)
- ヒープ確保が必要(Tuple は参照型なので)
名前が消えるという結構嫌な欠点の割に、Tuple クラスは使いやすいかというと微妙… Tuple.Create(1, “abc”) とかが煩雑。
データ移送(transport)用の型を作る
現状、out 引数や System.Tuple の欠点を回避しようとすると、普通に明示的な型定義が必要になります。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public struct TallyResult { public int Sum; public int Count; } | |
public TallyResult Tally(IEnumerable<int> values) { … } | |
var t = Tally(myValues); | |
Console.WriteLine($"Sum: {t.Sum}, count: {t.Count}"); |
各値の名前も消えず、非同期メソッドでも使えて、struct にしたのでヒープ確保も起きません。
型名にちゃんとした意味があれば、疑うべくもなく型を作るのがよい方法だと思います。「一時的」じゃなく、その後もずっと使うようなものであれば、もっと型を作る意義が出てくるでしょう。
ですが、この例の TallyResult (Tally メソッドの結果なんて、見りゃわかるだろ)なんて名前に特に意味もないわけで、あまりよい方法とは言えません。もちろん、SumAndCount (メンバーが Sum と Count なんだから見りゃわかる)なんて名前も論外。値も、だいたいすぐに sum と count に分解して使うようなものです。特定用途下に置いていい名前が付くことがあるかもしれませんが(我々のプログラム中では、A という概念が sum と count を必要とするので、これを A と命名しよう。とか)、それはそれで、別の用途に使えなくなります。
タプル構文
そこで、(System.Tuple ではなく、C# の言語機能として)タプル型を導入しようという提案が出ています。
書き方は、以下のような理由から、引数リストと似た構文になるようです。
- 引数リストは、背景での説明通り、概念的にはタプルに近い
- タプルを導入したい一番の理由が多値戻り値
- 引数と戻り値は、入ってくるものと出ていくもの、表裏一体
- 「新構文の追加」というより、「引数と戻り値の対称性の改善」と考えればよくて、現状の構文になじみやすい
タプル型
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public (int sum, int count) Tally(IEnumerable<int> values) { … } | |
var t = Tally(myValues); | |
Console.WriteLine($"Sum: {t.sum}, count: {t.count}"); |
C# では、いわゆる「名前付きタプル」だけ導入するつもりみたいです。一般にタプルというと、(int, string) みたいな、メンバー名のない、ほんと単なる値のグループ化を指したりするものですが、こういう「メンバー名のないタプル」はなし。
この例の (int sum, int count) で、 struct Anonymous { public int sum; public int count; } みたいな構造体が(型名なしで)作られる感じ。
タプルは、非同期メソッドの戻り値にも使えます。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public async Task<(int sum, int count)> TallyAsync(IEnumerable<int> values) { … } | |
var t = await TallyAsync(myValues); | |
Console.WriteLine($"Sum: {t.sum}, count: {t.count}"); |
タプル リテラル
リテラル用の構文を追加しなくても、一応、var t = new (int sum, int count) { sum = 0, count = 0 } とかいう書き方はできるでしょうが、あまりにも煩雑なので、ちゃんとリテラル構文も。
タプル型自体が「引数リストの定義」(仮引数リスト)みたいな書き方なんだから、タプル リテラルは「引数リストへの値の渡し方」(実引数リスト)みたいであるべきです。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(int sum, int count) result = (1, 2); // 左辺の型を見て | |
var result = (sum: 1, count: 2); // 名前付き引数の要領で、右辺から型推論 |
タプルの分解
値を(無名のまま)構造化(construction)するのがタプルなら、その逆、脱構造化(deconstruction)操作も欲しくなります。タプルはほんとに一時的なグループ化に使うことが多く、値を受け取る側は即座に、値を別々の変数に入れて使いたかったりします。var t = (sum: 1, count: 2) でタプル化できるんなら、(var sum, var count) = t;でタプルの分解をしたい。ちゃんとそのための構文も用意する予定です。ただし、分解は、変数宣言時のみ認めるのか、既存の変数に対する代入でも認めるのかはまだ未定だそうです。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
var t = (sum: 1, count: 2); // タプル構築 | |
(var sum, var count) = t; // タプル分解 | |
(sum, count) = t; // 既存変数へのタプル分解(これを認めるかどうかは未定) |
詳細
大まかな文法の提案に加えて、細かにもう少し詰めて考えないと行けないことがあります。
クラスか構造体か
構造体(値型)は、「ヒープ確保が必要ないけども、値のコピーが発生する」という性質上、一般的には、小さいデータや、他の型に埋め込んで使う(他の変数にコピーすることが少ない)場合に有効です。用途を絞らないと、逆に性能的に不利になることが多いです。そのためか、System.Tuple 型はクラス(参照型)ですし、F# のタプルはやっぱりクラスです。
しかし、C# 7でのタプルは多値戻り値を主な動機にしているのもあって、おそらくは非常に短命(構築して、戻り値として返して、すぐに分解して複数の変数で受けとる)になると予想されます。この場合であれば、構造体(値型)が有利に働くはず。なので今のところ、C# 7のタプルは構造体になりそうです。
書き換え可能性
「C# 7に向けて(1): 変数やフィールドの書き換え」で説明したように、値が書き変わらない保証がほしいときがあります。もちろん書き替えたい場合もあるわけで、選べるのが望ましいです。
同リンク先で話しているように、構造体の場合であれば、readonly 修飾(浅い不変性)を付けるだけで、フィールドの書き換えができなくなります(一方、クラスの場合はreadonlyだけではダメ。深い不変性が必要)。
なので、タプルには、構造体を採用した上で、何もつけなければ mutable (書き換えできる)、readonly を付けることで immutable (書き換え不能)にするようです。
値セマンティクス
構造体のような値型の場合、Equals と GetHashCode が、全フィールドの比較 / 全フィールドのハッシュ値からの計算で自動的に実装されます。これも、タプルを構造体にしたい動機になります。
ただ、この自動実装された Equals や GetHashCode は必ずしも効率的な結果にならないので、もしかしたらコンパイラーに(既定動作ではない)GetHashCode の生成をさせる必要があるかもしれません(要計測・要確認)。
タプルをフィールドとして利用
最たる動機が多値戻り値とはいえ、他にフィールドとして使いたい用途もあるでしょう。直接そういうフィールドを作ることは少ないと思いますが、例えば、ジェネリック型引数にタプルを指定することはあります。例えば複合キーや、複数値を持つ辞書を作りたいとき、Dictionary<(int key1, int key2), (int value1, int value2)> みたいな型を作る方法が有効ですが、この時、タプル型のフィールドができます。
ただ、タプルは mutable にするつもりなので、マルチスレッド動作の際には注意が必要です。他のオブジェクトのフィールドになることで、複数のスレッドが同時にタプルの値を書き替えようとして競合する場合がありえます。
変換
あるタプル型から別のあるタプル型に、「メンバーごとの変換」(member-wise conversions)が考えられます。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class Base {} | |
class Derived : Base {} | |
(Base x, Base y) a = (x: new Derived(), y: new Derived()); // covariance | |
(double sum, long count) b = (sum: 1, count: 2); // implicit conversion from int | |
(int s, int c) c = (sum: 1, count: 2); // renaming |
(issue ページでは「やる」とは名言されておらず、「できない理由はないよね」「やれるけど」「たぶんできるとよさそう」的書き方)
アセンブリをまたいでの単一化
こんな便利そうなタプルですが、これまで入らなかった主な理由というか、最大の課題は、アセンブリをどうやってまたぐか。
.NET では、異なるアセンブリの中に、完全に同名・同名前空間・同機能のクラスを定義できますが、これはアセンブリごとにそれぞれ別の型と認識されます。赤の他人が同名の別クラスを定義してしまったことによって、クラス利用者のコードが壊れるようなことがあってはいけないので当然といえば当然の処置。しかしこれが、アセンブリをまたいだ匿名クラス・匿名構造体の利用を難しくしています。タプルは実装上、匿名構造体になるわけで、この問題をクリアしないと、アセンブリを超えた利用が面倒です。
あり得る解決策は例えば、
- .NET ランタイムに手を入れて、別アセンブリ同名の型をほんとに同一視できるような機能を入れるか
- 例えば、ASP.NET 5 では、Assembly Neutral なインターフェイスというのを導入するようです
- コンパイラーが頑張って、相互変換コードを生成するか
とかになります。
ここはほんとに最大のネックなので、更なる検討が必要なところ。
匿名型との関係
C# 3.0で匿名型という構文が導入済みなわけですが、コンパイラーによる型生成という点でタプルはよく似ています。導入の動機(LINQ 用 ⇔ 多値戻り値用)や実装方法(クラス ⇔ 構造体)が違うので完全に統一するのは無理でしょうが、親和性を高めることはできます。そのために、匿名型の方にも手を入れることが考えられます。
- { string Name, int Age } みたいな書き方で型を明示的に書けるようにする(今は var (型推論)で受けるしかない)
- 戻り値の型やフィールドにも使えるようにする
- タプル同様、アセンブリをまたいだ利用についての検討が必要
- 分解(deconstruction) できるようにする
付加的な機能充実
タプル型を多値戻り値のために使うことを考えると、いくつか利便性のための機能が考えられます。
タプルのメンバーをメソッド本体内で使う
多値戻り値を、return (x, y); みたいに返すんじゃなくて、out 引数であるかのような書き方で返す構文(書き方が近いだけで実際には、ちゃんとタプルが作られる)が考えられます。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public (int sum, int count) Tally(IEnumerable<int> values) | |
{ | |
sum = 0; count = 0; | |
foreach (var value in values) { sum += value; count++; } | |
} |
out 引数同様、return ステートメント不要。その代り、やはり同様に、メソッドを抜けるまでに必ず値の代入が必要。
引数のスプラッティング
複数の引数に対して、1つのタプルで値を渡せるようにする(コンパイラーがタプルを分解して、各引数に渡す)ことをスプラッティング(splatting)というみたい。元々は3D CG方面の用語で、複数のテクスチャを1枚に合成する手法から来ているっぽい。おそらくは元の用途が、地面や壁にはねた飛沫(splat。要するにスプラッター)の表現だったからこの名前。
C# でも、タプル型を導入するのであればスプラッティングも欲しくなるでしょう。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(int sum, int count) Tally(IEnumerable<int> values) | |
{ | |
sum = 0; count = 0; | |
foreach (var value in values) { sum += value; count++; } | |
} | |
double Average(int sum, int count) => count == 0 ? 0 : sum / count; | |
var avg = Average(Tally(new[] { 1, 2, 3, 4, 5})); // タプル (int sum, int count) を分解して、Averabe(sum, count) に渡す |
Tally の戻り値は (int sum, int count) というタプル型。一方で、Average は引数が2つのメソッド。2つのメソッドをつなぐためにはタプルの分解が必要になります。この例のように、多値戻り値をそのまま別のメソッドの多値引数にしたいことは当然あるわけで、多値戻り値のためにタプルを導入するのであれば欲しい機能です。
この逆のパターン(unsplatting)もあります。普通、タプル型の引数を1つ受け取るようなメソッドは作らないでしょうが、ジェネリック型引数にタプル型を指定した場合には起こり得る話です。この場合に、複数の引数を渡して、1つのタプルに構築してからメソッドを呼び出してもらいたいです。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
var list = List<(string name, int age)>(); | |
list.Add("John Doe", 66); // "unsplatting" to a tuple |
C# 7に向けて(7): まだ具体案の見えていないもの
「C# Design Notes / テーマ」で「どういうことがしたいか」だけ列挙されているものの、具体的にどういう仕様を追加するかまではまったく決まっていないものがまだまだ結構あります。そんな具体案の見えていないものに対して、いくつか、issue ページができて、オープンなディスカッションを始めたようです。
あと、Issue に対してマイルストーンも切り始めたみたいで、「milestone: “C# 7 and VB 15″」で検索すると一覧が出てきます。
- [Proposal] Support System.Delegate as a generic constraint #158
- Proposal: bestest betterness #250
- Proposal: Declaration Expressions #254
- Proposal: support await in Catch and Finally in VB #256
- Proposal: Virtual extension methods #258
- Proposal: Nested local functions and type declarations #259
- Proposal: language support for async sequences #261
- Proposal: support an enum constraint on generic type parameters #262
- Declaration of ref/out parameters in lambdas without typename #303
ちなみに、#158 とか #303 とかは、C# チーム以外のコミュニティ提案のものです。
await in Catch and Finally in VB
C# 6で、await 演算子を catch/finally 句内に書けるようになって、多くの C# 開発者が歓喜した(C# 6の正式版リリースが待ち遠しい)と思います。
そして、「同時期の C# と VB の機能はだいたい一緒」なので、VB 14 でも… と思いきや、VB 側だけ間に合わなかったみたいでして。15 送りに…
bestest betterness
オーバーロード解決の話。
C# は同じ名前で引数違いのメソッド(メソッドのオーバーロード)を持てるわけですが、X(object) と X(string) に対して string の実引数を渡すときみたいに、どちらでも呼べるような場合があります。こういう場合、どちらのメソッドが呼ばれるかは、betterness rule (優れた方を呼ぶルール)というのがあって、型の一致度がよりよい方が呼ばれます。X(object) と X(string) の例でいうと、string の方が優先されます。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using static System.Console; | |
class Program | |
{ | |
static void Main() | |
{ | |
X(0); // object | |
X(""); // string | |
} | |
static void X(string x) => WriteLine("string"); | |
static void X(object x) => WriteLine("object"); | |
} |
まあ、いくつか、直感的に betterness から外れる(一見すると一致度が高く見える方が呼ばれない)ような場合もあるんですが、だいたいは継承が絡んでいたり、ポインターが絡んでいたりという、複雑な場合です。例えば、後から基底クラスにメソッドが増えた時に、派生クラスの利用者側コードを壊さないように、型の一致度よりも「派生クラス側にある」ということが優先される場合があります。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using static System.Console; | |
class Program | |
{ | |
static void Main() | |
{ | |
Derived.X(0); // object | |
Derived.X(""); // object。型よりも派生クラス側が優先 | |
Base.X(""); // string | |
} | |
} | |
class Base | |
{ | |
public static void X(string x) => WriteLine("string"); | |
} | |
class Derived : Base | |
{ | |
public static void X(object x) => WriteLine("object"); | |
} |
こういう、合理的な理由のある「betterness から外れるもの」はいいとして、いくつか、C# の新機能のせいで「よりよい方」が変わってしまったものや、バグで「よりよい方」が選ばれていなかったのを修正したものがあったりします。C#チームは、こういう変更を「better betterness」(betterness rule をよりよく準拠するようにしたよ)とか言っていたみたいです。
「C#の新機能紹介」的な記事ではめったに紹介されませんが、実、細かくこういう better betterness 変更が毎度あったりします。で、C# 7/VB 15 でもまた better better betterness (より、より、よく)するだろうと。ついに、bestest betterness (最上級の best をもう一段最上級にした、日本語でいう「御御御付け」みたいなふざけた単語)とか言いだす始末。
generic constraints
- [Proposal] Support System.Delegate as a generic constraint #158
- Proposal: support an enum constraint on generic type parameters #262
ジェネリック制約(ジェネリック型引数に対して、where 句で「このTはこのインターフェイスを実装していないとダメ」とか「このTは構造体でないとダメ」とかの制約をつけるもの)に、「Tはデリゲート」(つまり、T() で呼び出ししたい)とか、「Tは列挙型」(つまり、int との相互変換ができたり、| でビット フラグを作れたり)とかの制約を付けたいという話。
むっちゃ欲しい。
とはいえ、これって、現状の .NETランタイム/.NET の IL レベルでは対応していないので、ランタイム側に手を入れるか、何かよっぽど仰々しいコード生成でもするかしないと無理なはずで、結構難易度高そう。要望としては強いだけに、何とかクリアしてほしいものの。
Virtual extension methods
C# 3 で入った拡張メソッドはなかなかに便利なものですが、まあ、実態はただの静的メソッドなので、virtual な動作(変数の型ではなくて、その中身、インスタンスの実行時の型を見て処理を分岐)をすることはできません。
これに対して、virtual な動作ができる、拡張メソッド的な何かもあってもいいんじゃないかという提案。
今のところ、Java の default method (インターフェイスのメソッドに、既定の実装を与える構文)みたいなものと説明されていますが…
Java の default method だと、
- 利点: virtual な動作ができる。クラス側で実装を変えれる。
- 欠点: インターフェイスの作者しか実装を与えられない。LINQ みたいに、第三者が拡張できない。
なわけですけども…
Java の default method は、「拡張」が目的じゃなくて、「後からインターフェイスにメンバーを足しても、既存コードを壊さない」(古い Java や現状の C# では、一度 public にしてしまったインターフェイスにメンバーを足すのはご法度)というのが一番の目的だと思うんですよねぇ。同じようなものに「Virtual extension methods」という呼び名はあんまり与えてほしくないというか…
あと、default method 的なものを導入しようと思うと、.NET ランタイムに手を入れないと行けないはず。C# コンパイラーだけでは対応できない。
Nested local functions and type declarations
関数の中(メソッド、演算子、プロパティなどのbody内)で、メソッドやクラスを定義したいという話。
まあ、そうしたいことは昔からたびたびあるんですが。これ、C++ でもたびたび話題になるやつですよねぇ。C++ だと、確か、「変数宣言との区別があいまいになる」ってのが、この機能が入らない理由だったんじゃないかと。↓みたいな書き方が許されるので、「関数内関数は」これとの相性がよろしくない。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
struct X | |
{ | |
X() {} | |
}; | |
void main() | |
{ | |
X x(); // X 型の変数 x に対して、コンストラクター X() で初期化 | |
} |
C# だと大丈夫なのかなぁ。C++ と同じ、「型名 変数名」で変数宣言する言語ですけども。変数宣言時のコンストラクター呼び出しを X x = new X(); と書く分、行ける気はしますが。
async sequence
C# 5で入った非同期メソッド(async/await)は結構革命的な機能だったわけですが、この機能は「単一の値を返す」のにしか向いていなくて、シーケンスを返すにはいまいちです(LINQ みたいな遅延リスト的な挙動ができない。配列なり List なりを作って返すしかない)。
現状、非同期にシーケンスを返すには、Rx を使うのが一番ではあるんですが、これに対して await 演算子みたいな C# 言語レベルでのサポートはありません。そういう言語レベルのサポートも欲しいよねという話。
await と Rx があるんだから割かしすんなりできそうに見えて、決め手に欠けてなかなかよい仕様が出てこない感じ。「非同期」自体が難しいテーマですからねぇ。C# 5の非同期メソッドも「汎用性を考えるとこうすべきなんだけど、使い勝手とパフォーマンス面を考えるとこうせざるを得ず」みたいな決断の跡がいくつかあって。 非同期シーケンスという要望もその当時からずっとあって、今まで決め手が得られていない難しさがあります。
ラムダ式での ref/out 引数
ref/out の付かない通常の引数の場合型推論が効くのに、ref/out 付きの場合には型の明示が必須なの、何とかしろ。という話。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
delegate bool TryParse<T>(string text, out T result); | |
class Program | |
{ | |
static void Main() | |
{ | |
TryParse<int> parse1 = (string s, out int result) => int.TryParse(s, out result); // これは OK | |
TryParse<int> parse2 = (s, out int result) => int.TryParse(s, out result); // ダメ。1個だけ型名明示とかできない。 | |
TryParse<int> parse3 = (s, out result) => int.TryParse(s, out result); // ダメ。out 型推論とかない。この機能がほしい。 | |
} | |
} |
これは割かし低リスクっぽい。まあ、ref とか out 自体をそれほど使わないという辺りがネック(リスクも低いけども、メリットも少ない)。
C# 7に向けて(6): Pattern matching
先日の続き、C# 7の目玉になるであろう機能、pattern matching と record types
の片割れ、pattern matching の話。
昨日の record types (もう、自分が書くクラスの7割くらいは record types で書くことになりそうな勢い)と比べると出番は少ないでしょうが、なかなかに便利そうな機能です。
マッチング構文
以下のような感じで、オブジェクトの型や値のマッチングを、再帰的に行う構文が追加されます。
- 定数マッチング
- x is 1
- 定数との比較
- 下記の
- 型マッチング
-
- x is T t
- x が特定の型 T の時にマッチ
- その時、x を T にキャストしたものが t に入る
- プロパティ マッチング
- x is T { X is int , Y is Int }
- x の型についてマッチした後、そのプロパティに大してもマッチング
- 再帰マッチング
- x is T(var a, 1)
- is 演算子というものに展開される
- record types の場合はこの is 演算子もコンパイラーが生成
- ユーザー定義の is 演算子も書ける
- var を書いたところは、何とでもマッチした上で、その場所に入っている値を変数 a に代入する
- var x = new T(1, 2) みたいなので構成(composition)するのの逆で、x is T(var a, var b) みたいなので分解(decomposition)できる
- 何とでもマッチング(ワイルドカード)
- x is T(var a, *)
- 何でもいいし、var 不要(値をとりだす必要がない)場合には、そこに * を書く
最後の再帰マッチングについては、composition と decomposition がきっちり対応する書き方になるのが結構きれい。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// composition | |
var expr = Add(Mult(Const(2), X()), Const(1)); | |
// decomposition | |
switch (expr) | |
{ | |
case Add(Mult(var a, var x), var b): …; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
abstract class Expr; | |
class X() : Expr; | |
class Const(double Value) : Expr; | |
class Add(Expr Left, Expr Right) : Expr; | |
class Mult(Expr Left, Expr Right) : Expr; | |
class Neg(Expr Value) : Expr; |
is 演算子を書く以外に、switch ステートメントでもパターン マッチングできるように拡張したいようです。
パラダイムが変わるのか
僕がこういう感じのセクション タイトルを付けるときは、だいたい「えっ… 別に」という答え付きだったりするわけですが。
こういう機能が出てくると、ついつい「オブジェクト指向 VS 関数型」、「関数型の方がいい!」「オブジェクト指向から関数型へのパラダイムシフト!」的な流れになりがちですが、まあ、どっちも使うでしょう、普通に。
自分の最近書いたコードで言うと、ビュー側での表示の切り替えのコード。例えば、要所を抜き出しすと、以下のような
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/// <summary> | |
/// バトル中のアクション。 | |
/// </summary> | |
public abstract class BattleAction | |
{ | |
public abstract void ApplyTo(Unit u); | |
} | |
/// <summary> | |
/// 攻撃アクション。 | |
/// </summary> | |
public class Attack : BattleAction | |
{ | |
public int Power { get; set; } | |
public override void ApplyTo(Unit u) => u.Life -= Power; | |
} | |
/// <summary> | |
/// 回復アクション。 | |
/// </summary> | |
public class Heal : BattleAction | |
{ | |
public int Power { get; set; } | |
public override void ApplyTo(Unit u) => u.Life += Power; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System.Threading.Tasks; | |
/// <summary> | |
/// デモ用 モデルもどき。 | |
/// </summary> | |
public class SampleModel | |
{ | |
/// <summary> | |
/// コマンド実行。 | |
/// </summary> | |
public static async Task ExecuteCommandAsync() | |
{ | |
var selectedAction = await SelectCommandAsync(); | |
var target = await SelectTargetAsync(); | |
// モデル側は普通に仮想メソッドとか使って処理を掛けるのが楽 | |
selectedAction.ApplyTo(target); | |
// ほんとは ApplyTo でアクション結果を返してもらって、ビューに通知 | |
} | |
/// <summary> | |
/// コマンド選択。 | |
/// </summary> | |
private static Task<BattleAction> SelectCommandAsync() | |
{ | |
// ほんとはビューにメッセージを投げて、 | |
// コマンド入力ウィンドウでも開いて、ユーザー入力を待って、アクションを選択する | |
return Task.FromResult<BattleAction>(new Attack()); | |
} | |
/// <summary> | |
/// ターゲット選択。 | |
/// </summary> | |
private static Task<Unit> SelectTargetAsync() | |
{ | |
// ほんとはビューにメッセージを投げて、 | |
// ターゲット選択ウィンドウでも開いて、ユーザー入力を待って、ターゲットを選択する | |
return Task.FromResult(new Unit()); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/// <summary> | |
/// ユニット。 | |
/// </summary> | |
public class Unit | |
{ | |
public int Life { get; set; } | |
// ほんとは他に攻撃力、魔力、防御力、… などなど。 | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/// <summary> | |
/// デモ用 ビューもどき。 | |
/// </summary> | |
public class SampleView | |
{ | |
public static void ShowBattleAction(BattleAction action) | |
{ | |
// モデル側はビューの事情を知ったことではないので、処理はこっち側で分岐することに。 | |
// 型を見て分岐。 | |
var heal = action as Heal; | |
if (heal != null) ShowHeal(heal); | |
var attack = action as Attack; | |
if (attack != null) ShowAttack(attack); | |
} | |
private static void ShowHeal(Heal heal) | |
{ | |
// 回復アクションの結果表示 | |
} | |
private static void ShowAttack(Attack attack) | |
{ | |
// 攻撃アクションの結果表示 | |
} | |
} |
RPG ゲーム的なものを想像してもらって、コマンド選択で「たたかう」だの「まほう」だののコマンド選択式で何らかのアクションを起こすようなものの一部分を抜き出したものです(実際、これを大規模化した感じのコードを前に書きまして)。
いわゆるモデル側では、やっぱり、仮想メソッドを使って処理の切り替えをやる方がシンプルでいい実装だと思うんですよね。この例で言うと、Model.cs 内、ApplyTo を呼んでいる部分。
一方で、これをやるためには、オブジェクト自身がやりたい処理を全部把握している必要があります。ApplyTo (選択したアクションを、ターゲットに適用)の場合は、モデル自身がやるべきことを知っているので仮想メソッドにできる。ビュー側はそうはいきません。ビューのことはビューしか知らず、モデル側(BattleAction)内部のメソッドとしては処理を書けません。
で、結局、往々にしてビュー側コードはどうなるかというと… 型を見ての分岐。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public static void ShowBattleAction(BattleAction action) | |
{ | |
var heal = action as Heal; | |
if (heal != null) ShowHeal(heal); | |
var attack = action as Attack; | |
if (attack != null) ShowAttack(attack); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public static void ShowBattleAction(BattleAction action) | |
{ | |
if (action is Heal heal) ShowHeal(heal); | |
else if (action is Attack attack) ShowAttack(attack); | |
} |
as してから != null チェックだらけに(AsPatternMatching.cs みたいな)。
これに対して、C# 7で導入予定の pattern matching を使えば、型による分岐が楽になります(Cs7PatternMatching.cs)。
争点
結構大きな機能追加なので、まだまだこれから仕様を固めないと行けないところとか、もめそうなところとか結構多め。
- 匿名型どうしよう?
- record types と匿名型でアプローチが違いすぎるんだけども
- 配列、List、Dictionary のマッチング
- switch でいいの?
- switch っていう単語は今まで通りな legacy な switch であってほしい。マッチングには match キーワード導入しない?
- switch みたいなステートメントじゃなくて、式にしてほしい
- パターン マッチング式中での dynamic 型の扱い
- switch での完備性のチェック
- その3で説明した「completeness」
- パターン マッチングがあり得るパターンを漏れなく網羅しているかどうかをチェックしたいけども、できる?
- 再帰マッチングとプロパティ マッチングは混在させたい?
- ワイルドカードは * なの?
- F# とかでは _ だけども…
- _ の方がなじみはあるものの、C# では _ は普通に識別子に使える名前だし…