Last active
November 22, 2023 12:14
-
-
Save ufcpp/ce7b480cf78456dbc20b7cf592fa707f to your computer and use it in GitHub Desktop.
IReadOnlyDictionary の TValue に 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
using System.Collections; | |
using System.Runtime.CompilerServices; | |
var d = new Dictionary<int, string> { { 1, "one" }, { 2, "two" } }; | |
ICovariantDictionary<int, string> cd = d.AsCovariant(); | |
// TValue が out なので string → object 代入可能。 | |
ICovariantDictionary<int, object> id = cd; | |
foreach (var (k, v) in id) | |
{ | |
Console.WriteLine($"{k}: {v}"); | |
} | |
// IReadOnlyDictionary<TKey, TValue> が invariant なのでたびたび困ってる。 | |
// covariant なインターフェイスにしたいんだけど… | |
public interface ICovariantDictionary<TKey, out TValue> | |
{ | |
// covariant にするためには TValue の方が戻り値で、bool を out にする必要がある。 | |
TValue? TryGetValue(TKey key, out bool hasValue); | |
// TValue が out だと、 | |
// IEnumerable<KeyValuePair<TKey, TValue>> すら戻り値にできない。 | |
// 回避策として、enumerator を object で戻り値に返して、 | |
// MoveNext の方でこの object を引数で受け取る構造にする。 | |
object GetEnumerationState(); | |
// 同上、covariant にするため out bool。 | |
TValue? MoveNext(object state, out TKey? key, out bool hasNext); | |
} | |
// GetEnumerationState/MoveNext(state) 構造を通常の IEnumerable にするためのラッパー。 | |
public class CovariantDictionaryEnumerable<TKey, TValue>(ICovariantDictionary<TKey, TValue> parent) | |
: IEnumerable<KeyValuePair<TKey, TValue>>, IEnumerator<KeyValuePair<TKey, TValue>> | |
{ | |
private object? _state; | |
public KeyValuePair<TKey, TValue> Current { get; private set; } | |
object IEnumerator.Current => Current; | |
public bool MoveNext() | |
{ | |
_state ??= parent.GetEnumerationState(); | |
var value = parent.MoveNext(_state, out var key, out var hasNext); | |
Current = new(key!, value!); | |
return hasNext; | |
} | |
public void Dispose() { } | |
public void Reset() { } | |
public IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator() | |
{ | |
if(_state is null) return this; | |
else return new CovariantDictionaryEnumerable<TKey, TValue>(parent); | |
} | |
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); | |
} | |
// Dictionary<TKey, TValue> を ICovariantDictionary<TKey, TValue> にするためのラッパー。 | |
public class CovariantDictionary<TKey, TValue>(Dictionary<TKey, TValue> dictionary) : ICovariantDictionary<TKey, TValue> | |
where TKey : notnull | |
{ | |
public object GetEnumerationState() => dictionary.GetEnumerator(); | |
public TValue? MoveNext(object state, out TKey? key, out bool hasNext) | |
{ | |
// dictionary を IReadOnlyDictionary にする場合、この行は Unsafe.As に変更。 | |
// パフォーマンスを考えると Dictionary (クラス)用と IReadOnlyDictionary (インターフェイスごし)用に分けざるを得ないかも。 | |
ref var e = ref Unsafe.Unbox<Dictionary<TKey, TValue>.Enumerator>(state); | |
if (e.MoveNext()) | |
{ | |
key = e.Current.Key; | |
hasNext = true; | |
return e.Current.Value; | |
} | |
else | |
{ | |
key = default; | |
hasNext = false; | |
return default; | |
} | |
} | |
public TValue? TryGetValue(TKey key, out bool hasValue) | |
{ | |
hasValue = dictionary.TryGetValue(key, out var value); | |
return value; | |
} | |
} | |
public static class CovariantDictionaryExtensions | |
{ | |
public static CovariantDictionary<TKey, TValue> AsCovariant<TKey, TValue>(this Dictionary<TKey, TValue> d) where TKey : notnull => new(d); | |
public static CovariantDictionaryEnumerable<TKey, TValue> AsEnumerable<TKey, TValue> | |
(this ICovariantDictionary<TKey, TValue> d) | |
=> new(d); | |
public static CovariantDictionaryEnumerable<TKey, TValue> GetEnumerator<TKey, TValue> | |
(this ICovariantDictionary<TKey, TValue> d) | |
=> new(d); | |
} |
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.Runtime.CompilerServices; | |
var d = new Dictionary<int, string> { { 1, "one" }, { 2, "two" } }; | |
// IReadOnlyDictionary を介さず Dictionary 同士なら Unsafe.As 幾分か動いたりはするものの… | |
Dictionary<int, object> ud = Unsafe.As<Dictionary<int, string>, Dictionary<int, object>>(ref d); | |
// とりあえず foreach は可能。 | |
foreach (var (k, v) in ud) | |
{ | |
Console.WriteLine($"{k}: {v}"); | |
} | |
// もちろん set やっちゃうとやばい。 | |
ud[0] = 1.2; | |
// 変な値が表示される。 | |
// 場合によっては EntryPointNotFound とか AccessViolation とかになっても文句は言えない。 | |
Console.WriteLine(d[0].Length); | |
// IReadOnlyDictionary に渡すとアウト。 | |
IReadOnlyDictionary<int, object> id = ud; | |
foreach (var (k, v) in id) // ここで EntryPointNotFound。 | |
{ | |
Console.WriteLine($"{k}: {v}"); | |
} |
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.Collections; | |
using System.Diagnostics.CodeAnalysis; | |
var d = new Dictionary<int, string> { { 1, "one" }, { 2, "two" } }; | |
IReadOnlyDictionary<int, object> rd = d.Upcast(default(object)); | |
foreach (var (k, v) in rd) | |
{ | |
Console.WriteLine($"{k}: {v}"); | |
} | |
// IReadOnlyDictionary<TKey, TValue> が invariant なのでたびたび困ってる。 | |
// 自前で IReadOnlyDictionary<TKey, TDerived> を IReadOnlyDictionary<TKey, TBase> に変換するラッパーをかますことに… | |
public class UpcastDictionary<TKey, TDerived, TBase>(IReadOnlyDictionary<TKey, TDerived> dictionary) | |
: IReadOnlyDictionary<TKey, TBase> | |
where TKey : notnull | |
where TDerived : TBase | |
{ | |
// 素通しのメンバーは全然いい。 | |
public IEnumerable<TKey> Keys => dictionary.Keys; | |
public int Count => dictionary.Count; | |
public bool ContainsKey(TKey key) => dictionary.ContainsKey(key); | |
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); | |
// covariance が効くメソッドもいい。 | |
public TBase this[TKey key] => dictionary[key]; | |
public IEnumerable<TBase> Values => (IEnumerable<TBase>)dictionary.Values; | |
// 割と低コストで変換できるメソッドもまあ許せる。 | |
public bool TryGetValue(TKey key, [MaybeNullWhen(false)] out TBase value) | |
{ | |
if (dictionary.TryGetValue(key, out var derivedValue)) | |
{ | |
value = derivedValue; | |
return true; | |
} | |
else | |
{ | |
value = default!; | |
return false; | |
} | |
} | |
// enumerator がどうしてもアロケーションかかる… | |
public IEnumerator<KeyValuePair<TKey, TBase>> GetEnumerator() | |
{ | |
foreach (var (key, value) in dictionary) | |
{ | |
yield return new(key, value); | |
} | |
} | |
} | |
public static class DictionaryExtensions | |
{ | |
// 変換用の拡張メソッドを用意したいんだけど… | |
// 型推論、部分的にはかからなくて、このシグネチャだとどうやっても | |
// x.Upcast<TKey, TDerived, TBase>() | |
// みたいに書かないとダメ。つらい… | |
public static UpcastDictionary<TKey, TDerived, TBase> Upcast<TKey, TDerived, TBase>( | |
this IReadOnlyDictionary<TKey, TDerived> dictionary) | |
where TKey : notnull | |
where TDerived : TBase | |
=> new(dictionary); | |
// いくらかマシにするならダミー引数。 | |
// それでも x.Upcast(default(TBase)) になる。 | |
public static UpcastDictionary<TKey, TDerived, TBase> Upcast<TKey, TDerived, TBase>( | |
this IReadOnlyDictionary<TKey, TDerived> dictionary, | |
TBase? _) // 型推論用ダミー引数。 | |
where TKey : notnull | |
where TDerived : TBase | |
=> new(dictionary); | |
} |
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.Runtime.CompilerServices; | |
var d = new Dictionary<int, string> { { 1, "one" }, { 2, "two" } }; | |
// IReadOnlyDictionary を介さず Dictionary 同士なら Unsafe.As 幾分か動いたりはするものの… | |
Dictionary<int, object> ud = Unsafe.As<Dictionary<int, string>, Dictionary<int, object>>(ref d); | |
// とりあえず foreach は可能。 | |
foreach (var (k, v) in ud) | |
{ | |
Console.WriteLine($"{k}: {v}"); | |
} | |
// TryGetValue も動く。 | |
Console.WriteLine(ud.TryGetValue(1, out var x) ? x : null); | |
// もちろん set やっちゃうとやばい。 | |
ud[0] = 1.2; | |
// 変な値が表示される。 | |
// 場合によっては EntryPointNotFound とか AccessViolation とかになっても文句は言えない。 | |
Console.WriteLine(d[0].Length); | |
// IReadOnlyDictionary に渡すとアウト。 | |
IReadOnlyDictionary<int, object> id = ud; | |
foreach (var (k, v) in id) // ここで EntryPointNotFound。 | |
{ | |
Console.WriteLine($"{k}: {v}"); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment