Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions src/Transit/Impl/ListWrapper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace Transit.Net.Impl;

/// <summary>
/// A wrapper class for lists, to semantically preserve how the list should be serialized.
/// Normally things tagged with "list" would be interpreted as LinkedLists, but in C# we want to
/// use regular Lists for performance.
/// </summary>
internal sealed class ListWrapper : List<object?>;
78 changes: 69 additions & 9 deletions src/Transit/Impl/NullKeyDictionary.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
using System.Collections;
using System.Collections.Generic;

namespace Transit.Net.Impl;

/// <summary>
/// A dictionary that supports null keys, matching Java's HashMap behavior.
/// Used by cmap (composite-key map) read handling where null can be a valid key.
/// </summary>
internal sealed class NullKeyDictionary : IDictionary
internal sealed class NullKeyDictionary : IDictionary, IDictionary<object?, object?>, IReadOnlyDictionary<object?, object?>
{
private readonly Dictionary<object, object?> _inner = new();
private bool _hasNullKey;
Expand All @@ -26,14 +27,20 @@

public int Count => _inner.Count + (_hasNullKey ? 1 : 0);

public bool Contains(object? key)
public bool ContainsKey(object? key)
=> key is null ? _hasNullKey : _inner.ContainsKey(key);

public ICollection Keys
bool IDictionary.Contains(object? key) => ContainsKey(key);

bool ICollection<KeyValuePair<object?, object?>>.Contains(KeyValuePair<object?, object?> kvp) => kvp.Key is null
? _hasNullKey && EqualityComparer<object>.Default.Equals(kvp.Value, _nullValue)
: ((ICollection<KeyValuePair<object, object?>>)_inner).Contains(kvp);

Check warning on line 37 in src/Transit/Impl/NullKeyDictionary.cs

View workflow job for this annotation

GitHub Actions / build

Argument of type 'KeyValuePair<object?, object?>' cannot be used for parameter 'item' of type 'KeyValuePair<object, object?>' in 'bool ICollection<KeyValuePair<object, object?>>.Contains(KeyValuePair<object, object?> item)' due to differences in the nullability of reference types.

Check warning on line 37 in src/Transit/Impl/NullKeyDictionary.cs

View workflow job for this annotation

GitHub Actions / build

Argument of type 'KeyValuePair<object?, object?>' cannot be used for parameter 'item' of type 'KeyValuePair<object, object?>' in 'bool ICollection<KeyValuePair<object, object?>>.Contains(KeyValuePair<object, object?> item)' due to differences in the nullability of reference types.

public ICollection<object?> Keys
{
get
{
if (!_hasNullKey) return _inner.Keys;

Check warning on line 43 in src/Transit/Impl/NullKeyDictionary.cs

View workflow job for this annotation

GitHub Actions / build

Nullability of reference types in value of type 'Dictionary<object, object?>.KeyCollection' doesn't match target type 'ICollection<object?>'.

Check warning on line 43 in src/Transit/Impl/NullKeyDictionary.cs

View workflow job for this annotation

GitHub Actions / build

Nullability of reference types in value of type 'Dictionary<object, object?>.KeyCollection' doesn't match target type 'ICollection<object?>'.
var keys = new List<object?>(_inner.Count + 1);
foreach (var k in _inner.Keys) keys.Add(k);
keys.Add(null);
Expand All @@ -41,38 +48,85 @@
}
}

public ICollection Values
IEnumerable<object?> IReadOnlyDictionary<object?, object?>.Keys => Keys;

ICollection IDictionary.Keys => (ICollection)Keys;

public ICollection<object?> Values
{
get
{
if (!_hasNullKey) return (ICollection)_inner.Values;
if (!_hasNullKey) return _inner.Values;
var values = new List<object?>(_inner.Count + 1);
foreach (var v in _inner.Values) values.Add(v);
values.Add(_nullValue);
return values;
}
}

IEnumerable<object?> IReadOnlyDictionary<object?, object?>.Values => Values;

ICollection IDictionary.Values => (ICollection)Values;

public bool IsFixedSize => false;
public bool IsReadOnly => false;
public bool IsSynchronized => false;
public object SyncRoot => this;

public void Add(object key, object? value) => this[key] = value;

Check warning on line 76 in src/Transit/Impl/NullKeyDictionary.cs

View workflow job for this annotation

GitHub Actions / build

Nullability of reference types in type of parameter 'key' of 'void NullKeyDictionary.Add(object key, object? value)' doesn't match implicitly implemented member 'void IDictionary<object?, object?>.Add(object? key, object? value)' (possibly because of nullability attributes).

Check warning on line 76 in src/Transit/Impl/NullKeyDictionary.cs

View workflow job for this annotation

GitHub Actions / build

Nullability of reference types in type of parameter 'key' of 'void NullKeyDictionary.Add(object key, object? value)' doesn't match implicitly implemented member 'void IDictionary<object?, object?>.Add(object? key, object? value)' (possibly because of nullability attributes).

void ICollection<KeyValuePair<object?, object?>>.Add(KeyValuePair<object?, object?> kvp) => Add(kvp.Key, kvp.Value);

Check warning on line 78 in src/Transit/Impl/NullKeyDictionary.cs

View workflow job for this annotation

GitHub Actions / build

Possible null reference argument for parameter 'key' in 'void NullKeyDictionary.Add(object key, object? value)'.

Check warning on line 78 in src/Transit/Impl/NullKeyDictionary.cs

View workflow job for this annotation

GitHub Actions / build

Possible null reference argument for parameter 'key' in 'void NullKeyDictionary.Add(object key, object? value)'.

public void Clear() { _inner.Clear(); _hasNullKey = false; _nullValue = null; }
public void Remove(object key)

public bool Remove(object? key)
{
if (key is null) { _hasNullKey = false; _nullValue = null; }
else _inner.Remove(key);
if (key is null)
{
if (_hasNullKey)
{
_hasNullKey = false;
_nullValue = null;
return true;
}
return false;
}
return _inner.Remove(key);
}

void IDictionary.Remove(object key) => Remove(key);

bool ICollection<KeyValuePair<object?, object?>>.Remove(KeyValuePair<object?, object?> item)
=> throw new NotImplementedException();

public void CopyTo(Array array, int index) => throw new NotImplementedException();

void ICollection<KeyValuePair<object?, object?>>.CopyTo(KeyValuePair<object?, object?>[] array, int arrayIndex)
=> throw new NotImplementedException();

public IDictionaryEnumerator GetEnumerator() => new NullKeyEnumerator(this);

IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();

private sealed class NullKeyEnumerator : IDictionaryEnumerator
IEnumerator<KeyValuePair<object?, object?>> IEnumerable<KeyValuePair<object?, object?>>.GetEnumerator()
=> new NullKeyEnumerator(this);

public bool TryGetValue(object? key, out object? value)
{
if (key is null)
{
if (_hasNullKey)
{
value = _nullValue;
return true;
}
value = default;
return false;
}
return _inner.TryGetValue(key, out value);
}

private sealed class NullKeyEnumerator : IDictionaryEnumerator, IEnumerator<KeyValuePair<object?, object?>>
{
private readonly NullKeyDictionary _dict;
private readonly IEnumerator<KeyValuePair<object, object?>> _innerEnum;
Expand All @@ -93,6 +147,12 @@
public object? Value => Entry.Value;
public object Current => Entry;

KeyValuePair<object?, object?> IEnumerator<KeyValuePair<object?, object?>>.Current => _onNull
? new KeyValuePair<object?, object?>(null!, _dict._nullValue)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why null! when you're producing a value of object??

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, whoops. This was copied verbatim from a few lines above, where we create a DictionaryEntry object for similar reasons, and the signature of the DictionaryEntry constructor takes a non-null object for a key

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, OK, was hoping it wasn't some required C# thing because of the context or generic arguments or something.

: new KeyValuePair<object?, object?>(_innerEnum.Current.Key, _innerEnum.Current.Value);

public void Dispose() { }

public bool MoveNext()
{
if (_innerEnum.MoveNext()) { _onNull = false; return true; }
Expand Down
8 changes: 4 additions & 4 deletions src/Transit/Impl/ReadHandlers/ReadHandlers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -136,12 +136,12 @@ internal sealed class ListReadHandler : IListReadHandler
{
public object FromRepresentation(object representation) => representation;

public IListReader ListReader() => new LinkedListReader();
public IListReader ListReader() => new ListWrapperReader();

private sealed class LinkedListReader : IListReader
private sealed class ListWrapperReader : IListReader
{
public object Init() => new LinkedList<object>();
public object Add(object list, object item) { ((LinkedList<object>)list).AddLast(item); return list; }
public object Init() => new ListWrapper();
public object Add(object list, object item) { ((ListWrapper)list).Add(item); return list; }
public object Complete(object list) => list;
}
}
Expand Down
1 change: 1 addition & 0 deletions src/Transit/Impl/WriterFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ private static FrozenDictionary<Type, IWriteHandler> BuildDefaultHandlers()
[typeof(ITaggedValue)] = new TaggedValueWriteHandler(),
[typeof(ISet<>)] = new SetWriteHandler(),
[typeof(IEnumerable)] = new EnumerableWriteHandler(),
[typeof(ListWrapper)] = new EnumerableWriteHandler(),
[typeof(IList<>)] = listHandler,
[typeof(IDictionary<,>)] = new DictionaryWriteHandler(),
[typeof(NullKeyDictionary)] = new DictionaryWriteHandler(),
Expand Down
Loading