Skip to content

Instantly share code, notes, and snippets.

@ufcpp
Last active November 22, 2023 12:14
Show Gist options
  • Save ufcpp/ce7b480cf78456dbc20b7cf592fa707f to your computer and use it in GitHub Desktop.
Save ufcpp/ce7b480cf78456dbc20b7cf592fa707f to your computer and use it in GitHub Desktop.
IReadOnlyDictionary の TValue に out つけれないの毎度つらい
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);
}
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}");
}
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);
}
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