Skip to content

Instantly share code, notes, and snippets.

@ufcpp
Last active August 23, 2017 19:43
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ufcpp/64c2ed92f6edd81d9924c4c2caab2072 to your computer and use it in GitHub Desktop.
Save ufcpp/64c2ed92f6edd81d9924c4c2caab2072 to your computer and use it in GitHub Desktop.
どうにかして .NET でも depth subtyping できないかなぁ…
//#define UNABLE_TO_COMPILE
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
class Base { }
class Derived : Base { }
class Program
{
static void Main(string[] args)
{
// 子クラスを親クラスに代入するのは合法
Base x1 = new Derived();
// いわゆる coveriance
// out 付きのジェネリック引数 T がある場合、T の親子関係を引き継げる
IEnumerable<Base> x2 = (IEnumerable<Derived>)new Derived[0];
// タプルは一見、covariant に見えるんだけど…
// 実際には、これ、コンパイラーによる「メンバーごとに変換可能なものは変換してくれる」っていう特殊対応によるもの
(int, Derived) derivedTuple = (1, new Derived());
(int, Base) x3 = derivedTuple;
// タプル(System.ValueTuple)と同じものを自前で用意すると、↑の covariance が働かない
MyTuple<int, Derived> myDerivedTuple = new MyTuple<int, Derived>(1, new Derived());
#if UNABLE_TO_COMPILE
MyTuple<Base, Base> x4 = myDerivedTuple; // コンパイル エラー
#else
// こんな感じのコードを書かないといけない
// MyTuple<int, Base> と MyTuple<int, Derived> はメモリレイアウト同じだから丸コピーしたいのにできない
// コンストラクターから呼びなおし
MyTuple<int, Base> x4 = new MyTuple<int, Base>(myDerivedTuple.Item1, myDerivedTuple.Item2);
#endif
// でも、実のところ、Unsafe な手段で無理やり代入しても平気
// ジェネリック引数 T の親子関係に沿っている限り、これは安全なはず
// (こういう「メンバーごとに covariant なら型全体も covariant 扱いしていいよね」みたいなのを (レコード型の)depth subtyping って言うらしい)
MyTuple <int, Base> x5 = Unsafe.As<MyTuple<int, Derived>, MyTuple<int, Base>>(ref myDerivedTuple);
Console.WriteLine(x5.Item2.GetType()); // ちゃんと Derived が取れる
// 書き換えても、構造体(値型)なのでコピーが発生してて、myDerivedTuple の側は書き換わらない = 安全
x5.Item2 = new Base();
// とはいえ、あくまでコピーによって安全になっているので…
// ref でやっちゃうとまずい
ref MyTuple<int, Base> x6 = ref Unsafe.As<MyTuple<int, Derived>, MyTuple<int, Base>>(ref myDerivedTuple);
x6.Item2 = new Base();
Console.WriteLine(myDerivedTuple.Item2.GetType()); // Derive のはずなのに Base に書き換わってる = 危険
}
// 結局何が言いたいかというと、タプルとか、レコード型的な構造体なら variance 認めてほしいという話
interface CovariantTupleReturn<out T, in U>
{
// これは OK
T Method1();
void Mathod2(U arg);
// これは NG なんだけど…
// OK にできるんじゃないの?
// (ただし、やるなら CLR レベルでの修正が必要)
// (かつ、タプルだけ特別扱いってのも不格好。上記の通り、値型なら安全に扱えるはずなんだけど)
#if UNABLE_TO_COMPILE
(T, T) Method3();
void Method4((U, U) args);
#endif
}
}
// System.ValueTuple<T1, T2> とほとんど同じ内容の構造体
struct MyTuple<T1, T2>
{
public T1 Item1;
public T2 Item2;
public MyTuple(T1 item1, T2 item2)
{
Item1 = item1;
Item2 = item2;
}
}
//#define UNABLE_TO_COMPILE
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
class Base { }
class Derived : Base { }
class Program
{
static void Main()
{
NominalSubtyping();
try { EvilNominalSubtyping(); } catch (Exception ex) { Console.WriteLine(ex.Message); }
BehavioralSubtyping();
DepthSubtyping();
try { EvilDepthSubtyping(); } catch (Exception ex) { Console.WriteLine(ex.Message); }
ComplexDepthSubtyping();
}
#region 1. nominal subtyping
static void NominalSubtyping()
{
// おさらい
// Base に Derived を代入 → OK
// Derived に Base を代入 → NG (コンパイル エラー)
// こういう、明示的な派生関係があるものを nominal subtyping または nominative subtyping (指名的な派生型)っていう。
Base b = new Derived();
#if UNABLE_TO_COMPILE
Derived d0 = new Base();
#endif
}
// unsafe な手段を使えば上記制約は破れるんだけど、かなり不正な操作になる。
static Derived Cast(Base b) => Unsafe.As<Base, Derived>(ref b);
static void EvilNominalSubtyping()
{
// コンパイル通ってしまう。
Derived d = Cast(new Base());
// これで起こせる問題については長くなるので別途: https://gist.github.com/ufcpp/5ef97fc49eb9c0d2412377e9ef566b86
// ここでは「できたとしても破滅的な問題が起きる」とだけ。
// 軽めの問題:
// Derived から Derived へのキャストが失敗する謎挙動。
Console.WriteLine((Derived)(object)d); // InvalidCast 例外発生
}
#endregion
#region 2. behavioral subtyping
// out を付けると、
// Base ← Derived に nominal subtyping な関係があるとき、
// ICovariant<Base> ← ICovariant<Derived> にも subtyping が成り立つようになる。
// T の関係と同じ関係が ICovariant<T> にも成り立つことを指して covariant (共変的) (名詞形は covariance (共変性))という。
interface ICovariant<out T>
{
// 「out」の名前通り、戻り値にしか T を使ってはいけない。
T Value { get; }
#if UNABLE_TO_COMPILE
void M(T arg); // コンパイル エラー
#endif
}
// その逆。in を付けると、
// Base ← Derived に nominal subtyping な関係があるとき、
// IContravariant<Base> → ICovariant<Derived> と、逆向きの subtyping が成り立つようになる。
// 逆向きな関係なことを指して contravariant (反変的) (名詞形は contravariance (反変性))という。
interface IContravariant<in T>
{
// 「in」の名前通り、引数にしか T を使ってはいけない。
void M(T arg);
#if UNABLE_TO_COMPILE
T Value { get; } // コンパイル エラー
#endif
}
// co/contra-variance 合わせて、(ジェネリクスの)variance(変性)。
// 変性に基づいた subtyping な関係を behavioral subtyping (振る舞い的な派生型)って言うみたい。
class Variant<T> : ICovariant<T>, IContravariant<T>
{
public T Value => default(T);
public void M(T arg) { }
}
static void BehavioralSubtyping()
{
ICovariant<Base> b = new Variant<Derived>();
IContravariant<Derived> d = new Variant<Base>();
#if UNABLE_TO_COMPILE
// 逆はダメ
ICovariant<Derived> b0 = new Variant<Base>();
IContravariant<Base> d0 = new Variant<Derived>();
#endif
}
#endregion
#region 3. depth subtyping
// C# のジェネリクスの変性は、デリゲートもしくはインターフェイスに対してしか指定できないし、
// 他にも例えば以下のような制限あり。
interface ITupleCovariant<out T>
{
// 戻り値でしか使っていないのにコンパイル エラー!
// 構造体を介しているせい。
// 他の covariant な型を介してるなら大丈夫なものの、構造体は covariant にできない。
#if UNABLE_TO_COMPILE
// タプル(ValueTuple 構造体に展開される)もダメ
(bool, T) GetValue1();
// もちろん自分で作った構造体もダメ
Wrapper<T> GetValue2();
Optional<T> GetValue3();
#endif
}
struct Wrapper<T>
{
public T Value;
public Wrapper(T value) => Value = value;
}
struct Optional<T>
{
public bool HasValue { get; }
public T Value { get; }
public Optional(T value) { Value = value; HasValue = true; }
}
static Wrapper<Base> Cast(Wrapper<Derived> b) => Unsafe.As<Wrapper<Derived>, Wrapper<Base>>(ref b);
static void DepthSubtyping()
{
// でも、この手の型、別に無理やり covariant 的なキャストしても安全なはず。
#if UNABLE_TO_COMPILE
// 現在の文法だと認められてないので、まっとうな手段では無理
Wrapper<Base> b0 = new Wrapper<Derived>(new Derived());
#endif
// ということで邪道に手を染めるけども…
Wrapper<Base> b = Cast(new Wrapper<Derived>(new Derived()));
Console.WriteLine(b.Value); // Base に Derived が入っている(正しく subtyping されてる)ので問題ない
// Base を代入できてしまうものの、構造体(b への代入の時点でコピーされてる)なので大丈夫。
// 元の Wrapper<Derived> は書き換わらないからセーフ。
b.Value = new Base();
// つまり、コピーされてる限りには(それ自体は covariant ではない)構造体を介して、covariant なことをして平気。
// さっきの ITupleCovariant<out T> は、頑張れば #if をはずしてコンパイル可能にできるはず。
// (ちょっと頑張るの大変そう?)
// タプルとか、上記 Wrapper, Optional みたいな型は「レコード型(record type)」とか呼ばれる種類の型なんだけど、
// レコード型な構造体に対してなら subtyping を認めて問題ないはず。
// こういうのを(レコード型の)depth subtyping (レコードの各メンバーの「深さ」を見た派生型的な意味)っていう。
// 問題は、 .NET の型システムにとって、どういう時なら「レコード型だから depth subtyping 認めてOK」とかを判定するのが結構大変そうってこと。
}
#endregion
#region 3-1. depth subtyping の判定の難しさ - 単に構造体ってだけじゃダメ
// 3. の説明だと「構造体だったらコピーされるから大丈夫そう」くらいに見えるけど…
// そんな単純な条件では安全性保てない。
struct EvilWrapper<T>
{
class Ref { public T Value; }
// 内部的に一段階クラスを挟む。
Ref _ref;
public T Value { get => _ref.Value; set => _ref.Value = value; }
public EvilWrapper(T value) => _ref = new Ref { Value = value };
}
static EvilWrapper<Base> Cast(EvilWrapper<Derived> b) => Unsafe.As<EvilWrapper<Derived>, EvilWrapper<Base>>(ref b);
static void EvilDepthSubtyping()
{
// 3. の例と同じことを、上記の「一段クラスを挟んだ構造体」でやる。
EvilWrapper<Derived> d = new EvilWrapper<Derived>(new Derived());
EvilWrapper<Base> b = Cast(d);
Console.WriteLine(b.Value); // 読み出すだけなら大丈夫 (Base ← Derived)
// 構造体自体はコピー品なものの、内部的に一段階クラス(参照)を挟んでいるせいで、元の d.Value まで書き換わる。
// d.Value = new Base() されているに等しい状態なのでまずい(Derived ← Base)。
b.Value = new Base();
// Derived から Derived へのキャストが失敗する謎挙動。
Console.WriteLine((Derived)(object)d.Value); // InvalidCast 例外発生
}
// たぶん、全てのフィールドがTに関して covariant なら大丈夫そう?
// インターフェイス/デリゲートの covariance が「public な部分で戻り値にしか使ってない」なのに対して、
// 構造体の covariance は「全てのフィールドが満たす。読み書きは無関係」なのがちょっとわかりにくいけど。
//
// (C# コンパイラー的には判定できそうなものの、こういう安全性に関わる部分はランタイム上での判定も求められそう。
// フィールドを見ての合否判定してる構文は今もいくつかあるはずなので、割と何とかできるはず?)
#endregion
#region 3-2. depth subtyping の判定の難しさ - その他、要検討
struct ComplexType<T>
{
// 直接 T のフィールドを持っているのはいいとして…
public T Value;
// covariant なジェネリック インターフェイスもいいはず?
public IEnumerable<T> Items;
// 他のレコード的な型の入れ子はいいはず?
public (bool hasValue, T value) Optional;
}
static ComplexType<Base> Cast(ComplexType<Derived> b) => Unsafe.As<ComplexType<Derived>, ComplexType<Base>>(ref b);
static void ComplexDepthSubtyping()
{
ComplexType<Derived> d = new ComplexType<Derived>
{
Value = new Derived(),
Items = new List<Derived> { new Derived(), new Derived() },
Optional = (true, new Derived()),
};
ComplexType<Base> b = Cast(d);
// 以下、いずれも Base ← Derived なので OK。
Console.WriteLine(b.Value);
Console.WriteLine(string.Join(", ", b.Items));
Console.WriteLine(b.Optional);
// 以下、いずれもコピー品が書き換わるだけなので、d には影響なし = 安全。
b.Value = new Base();
b.Items = new Base[0];
b.Optional.value = new Base();
// d は書き換わってない
Console.WriteLine(d.Value);
Console.WriteLine(string.Join(", ", d.Items));
Console.WriteLine(d.Optional);
}
// 元々は depth subtyping を認めてもよさそうな型があったとして、
struct Version1<T>
{
private T _value;
public T Value => _value;
}
// ↓
// バージョンアップで private なところだけ書き換えて、ダメな感じに変化することがあり得る。
// (なので、「フィールドを見て暗黙的に判定」はまずい。おそらく、depth subtyping 可能な型には何らかの修飾が必要。)
struct Version2<T>
{
private class Ref { public T Value; }
private Ref _ref;
// public な部分は Version1 のまま。
public T Value => _ref.Value;
}
#endregion
}
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
class Base { }
class Derived : Base { }
// A struct all of whose fields are covariant could be covariant
struct CovariantStruct<T>
{
public T Value;
public IEnumerable<T> Items;
public (bool isValid, T value) Optional;
// Function members don't matter.
// `T` can be used for parameters.
public void Set(T value) => Value = value;
}
class Program
{
// Force to bitwise-copy with the Unsafe class.
static CovariantStruct<Base> BitwiseCopy(CovariantStruct<Derived> d) => Unsafe.As<CovariantStruct<Derived>, CovariantStruct<Base>>(ref d);
static void Main(string[] args)
{
CovariantStruct<Derived> d = new CovariantStruct<Derived>
{
Value = new Derived(),
Items = new List<Derived> { new Derived(), new Derived() },
Optional = (true, new Derived()),
};
// So far, conversion from CovariantStruct<Derived> to CovariantStruct<Base> requires unsafe context.
// However, this is safe.
CovariantStruct<Base> b = BitwiseCopy(d);
// All field accesses are safe (covariant).
Console.WriteLine(b.Value);
foreach (var item in b.Items) Console.WriteLine(item);
Console.WriteLine(b.Optional.value);
// This rewrite is safe because `b` is a copied value.
b.Set(new Base());
b.Items = new Derived[0];
b.Optional = (false, new Base());
// The original value `d` is not changed.
Console.WriteLine(d.Value);
foreach (var item in d.Items) Console.WriteLine(item);
Console.WriteLine(d.Optional.value);
}
}
// Proposal:
interface IVariant<out T, in U>
{
// OK
T GetValue();
void SetValue(U value);
// Error in the current C#/CLR.
// However these could be safe.
// I'd like covariant structs can be used for return types in covariant interfaces/delegates.
CovariantStruct<T> GetCovariantValue();
void SetContravariantValue(CovariantStruct<U> value);
}
// Open Question:
// How should "covariant struct" be defined
// 1. explicit or implicit?
// Version 1.
// This can be covariant.
struct X<T>
{
public T Value { get; set; }
}
interface I<out T>
{
// OK
X<T> GetX();
}
// ↓
// Version 2.
// This is no longer covariant.
struct X<T>
{
private StrongBox<T> _box;
public T Value { get => _box.Value; set => _box.Value = value; }
}
interface I<out T>
{
// NG. breaking change
X<T> GetX();
}
// Should the language introduce some modifier? such as `struct X<out T>` (similar to blittable types)
// 2. Syntax
// 2-1. `out` modifier on type parameters?
struct X<out T>
{
// This can be covariant because its field is covariant
public T _value;
// even though the type parameter `T` is used as parameters and set-accessors.
public T Value { get => _value; set => _value = value; }
public void SetValue(T value) => _value = value;
}
interface IVariant<out T, in U>
{
CovariantStruct<T> GetCovariantValue();
void SetContravariantValue(CovariantStruct<U> value);
}
// pros: Consitent with the modifier for interface/delegate.
// pros: No new keyword is needed.
// cons: It's not actually "out". Both "in" and "out" are allowed.
// 2-2. `out` modifier on structs?
out struct X<T1, T2>
{
public T1 Item1;
public T2 Item2;
}
out struct Y<T1, T2>
{
// covariant with T1
public T1 Item1;
// but invariant with T2
public StrongBox<T2> Item2; // should be error. how?
}
// 2-3. other keywords?
struct X<covariant T1, T2>
{
public T1 Item1;
public StrongBox<T2> Item2;
}
struct X<variant T1, T2>
{
public T1 Item1;
public StrongBox<T2> Item2;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment