7月のデザインノートが2件ほど。
これ関連の作業がひと段落したところでまとめて清書して表に出したって感じですかねぇ。 この辺りの話、かなりの割合がもう実装されててマージされてたりします。
先週、dots.をお借りしてこんなイベントやってたわけですが
最新のmasterブランチの取ってきてビルドして実行してみると、大体この仕様通りになってる感じ。
さて、どんな感じの仕様かというと…
タプル型のメンバー名は省略・名前付きの混在可能
こんなコードでOKですって。
var t = (1, y: 2); // infers (int, int y)
(int x, int) t = (1, 2);
ちなみに、名前を省略したところは、ValueTuple
がたの本来のメンバーである x.Item1
とかの名前で参照できます。
ITuple
タプル型みたいな「単に複数のデータを寄せ集めただけ」な型に対して、インデックスでメンバー参照したくなることがあります。
ValueTuple
型はそのために、以下のようなインターフェイスを実装すべきじゃないかという話に。
interface ITuple
{
int Size;
object this[int i] { get; }
}
タプル型の分解に使いたいそうで。
ValueTuple
型はこれを実装すべきだとは思うものの、名前にはまだ議論の余地あり。
インターフェイス名もIDeconstructable
とかがいいかもしれないし。
要素数のプロパティもLength
とかCount
とかもあり得るし。
var型がある場合。
C#のvarは、文脈キーワード(特定の文脈でだけキーワード扱いされる)です。var
って名前のクラスがあると、クラス名として認識される。
で、タプル型の分解構文で以下のような書き方を認めることになるわけですが、
var (x, y) = e;
ここで、var
クラスがあった場合どうなるべきか。
class var {}
var (x, y) = e;
ちなみに、世の中には、わざわざこういうvar
クラスを用意しておくことで、型推論のvarを使わせない(コンパイル エラーにさせる)トリッキーな運用をしている人もいるそうで。C#チーム的には「(その良し悪しは置いといて)そういう運用も認めるべきでしょう」という感じ。
そういう背景もあって、タプル型の分解におけるvarでも、var
クラスがあったらコンパイル エラーにするみたい。
var メソッド
じゃあ、メソッドの場合はどうか。分解代入の構文、メソッド呼び出しに似ているので、以下のような書き方ができてしまいます。
ref int var(int x, int y);
var(x, y) = e; // deconstruction or call?
参照戻り値なメソッドへの代入(参照先への代入)か、分解代入か、どちらにするべきか。
常に分解代入の方を選ぶそうです。メソッドの方を呼びたい場合は @var
って書けばできます。
partialクラスでのインターフェイス
partialクラスの場合、複数の宣言で、同じインターフェイスを継承できたりします。 ここで、じゃあ、メンバー名違いの同じ型のインターフェイスを継承してしまった場合はどうするべきか。
partial class C : IEnumerable<(string name, int age)> { ... }
partial class C : IEnumerable<(string fullname, int)> { ... }
タプル型は、内部的には全部ValueTuple
構造体に変換されます。
名前は属性に残るだけ。
で、じゃあ、上記の名前違いのインターフェイスは別の型なのか同じ型なのかよくわからず。
紛らわしいのでコンパイル エラーにすべきでしょう。
逆に、メンバー名も含めて全一致している場合だけは、複数のpartial宣言に書いても大丈夫。
もう少し面倒なケースは、多重継承(インターフェイスであればC#でも多重継承が可能)。 以下の場合はどうすべきか。
interface I1 : IEnumerable<(int a, int b)> {}
interface I2 : IEnumerable<(int c, int d)> {}
interface I3 : I1, I2 {} // what comes out when you enumerate?
class C : I1 { public IEnumerator<(int e, int f)> GetEnumerator() {} } // what comes out when you enumerate?
現状、これもコンパイル エラーにする案で進めてるみたい。 できてそこまで大きなメリットもなさそうなので、複雑化させない方向に倒すという感じ。 もし、将来的にこれを認めたくなるような重要な利用シナリオが見つかったりした場合、それはその時に考える。
タプル リテラルの分解
null (全ての参照型に代入可能)とか、1 (int
、short
, byte
辺りのどれか不明瞭)とか、リテラルの場合、型があいまいなものがあります。
その分解はちゃんと働くべきか。
(string x, byte y, var z) = (null, 1, 2);
できるべきだろうとのこと。
各要素ごとに並べて書いた時と同じ挙動になるべき。上記コードであれば、まあ、↓みたいなのと同じ解釈をすべき。
string x = null;
byte y = 1;
var z) = 2;
ただし、これが逐次実行されるわけじゃなくて、一斉に代入が起きる。つまり、swapに使っても差し支えないようなにはなってる。
(x, y) = (y, x); // swap!
タプル型の中のvar
「タプル型の変数宣言」と「分解代入」は非常に似た構文になるわけですが。
(int x, int y) = GetTuple(); // 分解
(int x, int y) t = GetTuple(): // タプル型の変数宣言
じゃあ、以下の構文(これも似て非なるもの)の場合はどうなるべきか。
(var x, var y) = GetTuple(); // これは分解代入時の型推論
(var x, var y) t = GetTuple(): // varなタプル型。これは認めるべき?
で、結論的には、この後者は認めないとのこと。
分解代入の戻り値の型は void?
C#では、代入は式です。どこにでも書けます…
var x = 1;
var y = (x = 2) * x;
まあ、ろくでもないんですが。副作用を伴う式とか割かし害悪。C言語を参考にしすぎたところですね。とはいえ、今更変更できません。
例えばの話、forステートメントの中には式を書くことになっているので、以下のようなコードを書きたければ、タプルの分解代入も式でないといけないそうです。
for (... ;; (current, next) = (next, next.Next)) { ... }
とはいえ、実のところ、「戻り値がvoidの式」という扱いにすれば、forステートメントの中で使えつつ、さっきのろくでもないy = (x = 2) * x
みたいなコードをなくせたりします。
ということで、voidであるべき?
まあ、これも、既存の代入式との一貫性がなくなるので、voidではなく、タプル型を返すべきだと思ってるみたいです。 C# 7では実装しなさそうだけど、後々は、分解代入の結果を、再度タプル構築して戻り値に返すべきだと思っているとのこと。
参考までに: Swift
ちなみに、Swiftはほんとに、代入は戻り値がvoidの式みたいです。
y = (x = 2) * x
なんていうクソコードは認めません。
その割にインクリメント・デクリメントがあった y = ++x * x
とか書けたわけですが。
そりゃ、forステートメントもインクリメントもなくしたくもなります(Swift 3で破壊的変更してまでなくす予定)。
分解を変換として、変換を分解として
分解代入と型変換はある程度似た構文です。分解は、タプル型への変換的な雰囲気があります。似てるのあれば、いっそある程度統一性を持たせるべき?
まあ、そうしない方がよさそう。分解(コンパイル結果的にはDeconstruct
メソッドの呼び出し)は型変換的に扱われるべきじゃない。
匿名型
匿名型({ X = 1, Y = "a" }
みたいなやつ)はDeconstruct
メソッドやITuple
インターフェイス実装を持つべき?
そうでもなさそう。実装しても、今のところ有用な利用シナリオが思い当たらないとのこと。 欲しくなる場面もなくはないけど、そういう場面では大体タプル型を使えば解決しそう。
分解代入時のワイルドカード
ワイルドカードってのは、要するに、要らない部分を読み飛ばす機能。
(var x, var y, *) = (1, 2, 3);
こういうコードで、3を読み飛ばすために使いもしないダミー変数を用意する必要はありません。
C#的に、こういう機能を入れるべきだろうとは思ってるみたい。 ただし、たぶん、C# 8になる(7には入らない。パターン マッチングと同時期に入る予定)。
あと、ワイルドカードのために使う記号はたぶん *
。
関数型言語の類だと _
を使うことが多いんですが、C#では _
が有効な識別子になっちゃうので。
既存コードの意味を変えてまではこの記号は使わないみたい(コード解析をきっちりやれば不可能ではないけど、そうまでするかという話)。
double型に対するswitch
パターン マッチングが入った暁には、double
型の変数もswitchに使えるわけですが。
ここで問題になるのは、double
型の等値判定。
NaN(Not a Number)の扱いどうするの?とか、実は==
とEquals
でNaNとの比較結果が違ったりするけどどうする?とか。
==
とEquals
の違いというと、int
の1とdouble
の1.0が等値判定とかも。前者はtrueになるけど、後者はfalse。
Equals
の側を使いそう。