Skip to content

Commit

Permalink
// WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
mikernet committed Jul 26, 2023
1 parent 42e4112 commit 77d039f
Show file tree
Hide file tree
Showing 25 changed files with 148 additions and 137 deletions.
14 changes: 4 additions & 10 deletions .github/workflows/build-and-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,8 @@ jobs:
uses: actions/setup-dotnet@v3
with:
dotnet-version: |
3.0.103
3.1.426
5.0.408
6.0.406
7.0.200
6.0.x
7.0.x
- name: Clean
run: dotnet clean --configuration Debug && dotnet nuget locals all --clear
working-directory: Source
Expand All @@ -45,11 +42,8 @@ jobs:
uses: actions/setup-dotnet@v3
with:
dotnet-version: |
3.0.103
3.1.426
5.0.408
6.0.406
7.0.200
6.0.x
7.0.x
- name: Clean
run: dotnet clean --configuration Release && dotnet nuget locals all --clear
working-directory: Source
Expand Down
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,12 @@ This package is part of our **Singulink Libraries** collection. Visit https://gi

The packages are available on NuGet - simply install the **Singulink.Collections**, **Singulink.Collections.Abstractions** and/or **Singulink.Collections.Weak** packages.

**Supported Runtimes**: Anywhere .NET Standard 2.0+ is supported, including:
- .NET Core 2.0+
- .NET Framework 4.6.1+
- Mono 5.4+
- Xamarin.iOS 10.14+
- Xamarin.Android 8.0+
**Supported Runtimes**: Anywhere .NET Standard 2.0 is supported, including:
- .NET
- .NET Framework
- Mono / Xamarin

End-of-life runtime versions that are out of support are not tested or supported by this library.

## Usage

Expand Down
49 changes: 25 additions & 24 deletions Source/Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,30 +1,31 @@
<Project>
<PropertyGroup>
<LangVersion>11.0</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<AnnotatedReferenceAssemblyVersion>6.0.0</AnnotatedReferenceAssemblyVersion>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
<PropertyGroup>
<LangVersion>11.0</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<AnnotatedReferenceAssemblyVersion>6.0.0</AnnotatedReferenceAssemblyVersion>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>

<PropertyGroup Condition="'$(Configuration)' == 'Release'">
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>

<ItemGroup>
<!-- Fody -->
<PackageReference Include="RuntimeNullables.Fody" Version="1.0.5" PrivateAssets="all" />
<ItemGroup>
<PackageReference Include="RuntimeNullables.Fody" Version="1.0.5" PrivateAssets="all" />
</ItemGroup>

<!-- Analyzers -->
<PackageReference Include="DotNetAnalyzers.DocumentationAnalyzers" Version="1.0.0-beta.59" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="7.0.1" PrivateAssets="all" />
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.435" PrivateAssets="all" />
<PackageReference Include="Roslynator.Analyzers" Version="4.3.0" PrivateAssets="all" />
<ItemGroup Label="Analyzers">
<PackageReference Include="DotNetAnalyzers.DocumentationAnalyzers" Version="1.0.0-beta.59" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="7.0.1" PrivateAssets="all" />
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.435" PrivateAssets="all" />
<PackageReference Include="Roslynator.Analyzers" Version="4.3.0" PrivateAssets="all" />
</ItemGroup>

<!-- Nullable Annotations -->
<PackageReference Include="TunnelVisionLabs.ReferenceAssemblyAnnotator" Version="1.0.0-alpha.160" PrivateAssets="all">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageDownload Include="Microsoft.NETCore.App.Ref" Version="[$(AnnotatedReferenceAssemblyVersion)]"/>
</ItemGroup>
<ItemGroup Label="Nullable Annotations" Condition="'$(TargetFramework)' == 'netstandard2.0' Or '$(TargetFramework)' == 'netstandard2.1'">
<PackageReference Include="TunnelVisionLabs.ReferenceAssemblyAnnotator" Version="1.0.0-alpha.160" PrivateAssets="all">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageDownload Include="Microsoft.NETCore.App.Ref" Version="[$(AnnotatedReferenceAssemblyVersion)]"/>
</ItemGroup>
</Project>
5 changes: 5 additions & 0 deletions Source/Directory.Build.targets
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<Project>
<ItemGroup>
<None Include="buildTransitive\package.targets" Pack="true" PackagePath="buildTransitive\$(TargetFramework)\$(PackageId).targets"/>
</ItemGroup>
</Project>
2 changes: 1 addition & 1 deletion Source/Singulink.Collections.Abstractions/IReadOnlySet.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// IReadOnlySet Polyfill for netstandard TFMs

#if !NETSTANDARD
#if NET

using System.Runtime.CompilerServices;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netstandard2.0;net5.0</TargetFrameworks>
<TargetFrameworks>netstandard2.0;net6.0</TargetFrameworks>
<RootNamespace>Singulink.Collections</RootNamespace>
<Version>1.0</Version>
<Version>2.0</Version>
<Authors>Singulink</Authors>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<Copyright>© Singulink. All rights reserved.</Copyright>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>netstandard2.0;netstandard2.1;netcoreapp3.0;net5</TargetFrameworks>
<TargetFrameworks>netstandard2.0;netstandard2.1;net6.0</TargetFrameworks>
<RootNamespace>Singulink.Collections</RootNamespace>
<Authors>Singulink</Authors>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
Expand All @@ -13,7 +13,7 @@
<SignAssembly>true</SignAssembly>
<AssemblyOriginatorKeyFile>key.snk</AssemblyOriginatorKeyFile>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<Version>1.2</Version>
<Version>2.0</Version>
</PropertyGroup>

<ItemGroup>
Expand All @@ -27,5 +27,4 @@
<ItemGroup>
<AdditionalFiles Include="stylecop.json" />
</ItemGroup>

</Project>
5 changes: 0 additions & 5 deletions Source/Singulink.Collections.Weak/WeakCollection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,7 @@ namespace Singulink.Collections
/// </remarks>
public sealed class WeakCollection<T> : IEnumerable<T> where T : class
{
#if NET || NETSTANDARD
private readonly HashSet<WeakReference<T>> _entries = new();
#else
// Use dictionary instead of HashSet because HashSet can't remove while enumerating before net5.
private readonly Dictionary<WeakReference<T>, Void> _entries = new();
#endif

private int _autoCleanAddCount;
private int _addCountSinceLastClean;
Expand Down
28 changes: 14 additions & 14 deletions Source/Singulink.Collections.Weak/WeakValueDictionary.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,20 @@ namespace Singulink.Collections
/// manner) then all accesses must be synchronized with a full lock.
/// </summary>
/// <remarks>
/// <para>On .NET Core 3+, internal entries for garbage collected values are removed as they are encountered (i.e. if a key lookup is performed on a garbage
/// collected value or if keys/values are enumerated over). This is not the case on .NET Standard targets like .NET Framework and earlier versions of .NET
/// Core. You can perform a full clean by calling the <see cref="Clean"/> method or configure automatic cleaning after a set number of add operations by
/// setting the <see cref="AutoCleanAddCount"/> property.</para>
/// <para>On .NET targets, internal entries for garbage collected values are cleaned as they are encountered (i.e. when a key lookup is performed on a
/// garbage collected value or key/value pairs are enumerated over). This is not the case on other .NET Standard targets (i.e. .NET Framework). You can perform
/// a full clean by calling the <see cref="Clean"/> method or configure automatic cleaning after a set number of add operations by setting the <see
/// cref="AutoCleanAddCount"/> property.</para>
/// </remarks>
public class WeakValueDictionary<TKey, TValue> : IEnumerable<KeyValuePair<TKey, TValue>>
where TKey : notnull
where TValue : class
{
#if NETSTANDARD2_0
#if NET
private readonly Dictionary<TKey, WeakReference<TValue>> _entryLookup;
#else
private Dictionary<TKey, WeakReference<TValue>> _entryLookup;
private int _capacity;
#else
private readonly Dictionary<TKey, WeakReference<TValue>> _entryLookup;
#endif
private int _autoCleanAddCount;
private int _addCountSinceLastClean;
Expand Down Expand Up @@ -119,7 +119,7 @@ public bool TryGetValue(TKey key, [NotNullWhen(true)] out TValue? value)
{
if (entry.TryGetTarget(out value))
return true;
#if NETCOREAPP
#if NET
else
_entryLookup.Remove(key);
#endif
Expand Down Expand Up @@ -246,10 +246,10 @@ public void Clear()
/// </summary>
public void Clean()
{
#if NETSTANDARD
var staleKvps = _entryLookup.Where(kvp => !kvp.Value.TryGetTarget(out _)).ToList();
#else
#if NET
var staleKvps = _entryLookup.Where(kvp => !kvp.Value.TryGetTarget(out _));
#else
var staleKvps = _entryLookup.Where(kvp => !kvp.Value.TryGetTarget(out _)).ToList();
#endif

foreach (var kvp in staleKvps)
Expand All @@ -266,14 +266,14 @@ public void Clean()
/// </summary>
public void TrimExcess()
{
#if NETSTANDARD2_0
#if NET
_entryLookup.TrimExcess();
#else
if (_capacity > _entryLookup.Count * 2)
{
_entryLookup = new Dictionary<TKey, WeakReference<TValue>>(_entryLookup, _entryLookup.Comparer);
_capacity = _entryLookup.Count;
}
#else
_entryLookup.TrimExcess();
#endif
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<Project>
<Target Name="CustomAfterBuildTarget" AfterTargets="Build">
<Message Text="Hello from CustomAfterBuildTarget" Importance="high" />
</Target>
</Project>
1 change: 1 addition & 0 deletions Source/Singulink.Collections.sln
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{79867FED-529B-4BE8-BF2B-7CF02BC48A28}"
ProjectSection(SolutionItems) = preProject
.editorconfig = .editorconfig
..\.github\workflows\build-and-test.yml = ..\.github\workflows\build-and-test.yml
Directory.Build.props = Directory.Build.props
..\README.md = ..\README.md
EndProjectSection
Expand Down
25 changes: 18 additions & 7 deletions Source/Singulink.Collections/HashSetDictionary.ReadOnlyValueSet.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,15 +54,15 @@ internal ReadOnlyValueSet(HashSetDictionary<TKey, TValue> dictionary, TKey key,
/// <see cref="Key"/> in <see cref="Dictionary"/> are modified.
/// </summary>
/// <remarks>
/// <para>References to transient sets should only be held for as long as a series of multiple consequtive read-only operations need to be performed on
/// <para>References to transient sets should only be held for as long as a series of multiple consecutive read-only operations need to be performed on
/// them. Once the values associated with <see cref="Key"/> are modified, the transient set's behavior is undefined and it may contain stale
/// values.</para>
/// <para>It is not beneficial to use a transient set for a single read operation, but when multipe read operations need to be done consequtively,
/// slight performance gains may be seen from performing them on a transient read-only set instead of directly on a <see cref="ValueSet"/> or <see
/// cref="ReadOnlyValueSet"/>, since the transient set does not check if it needs to synchronize with a newly attached value set in the
/// dictionary.</para>
/// <para>It is not beneficial to use a transient set for a single read operation, but when multiple read operations need to be done consecutively
/// (including enumerating over multiple values), slight performance gains may be seen from performing them on the transient read-only set instead of
/// directly on the <see cref="ValueSet"/> or <see cref="ReadOnlyValueSet"/>, since the transient set does not check if it needs to re-synchronize with
/// the dictionary.</para>
/// </remarks>
public ReadOnlyHashSet<TValue> AsTransient()
public ReadOnlyHashSet<TValue> AsTransientReadOnly()
{
var set = GetSet();

Expand Down Expand Up @@ -130,7 +130,18 @@ private protected HashSet<TValue> GetSet()
return _lastSet.Count > 0 ? _lastSet : UpdateAndGetValues();

[MethodImpl(MethodImplOptions.NoInlining)]
HashSet<TValue> UpdateAndGetValues() => _dictionary.TryGetValues(_key, out var valueSet) ? _lastSet = valueSet._lastSet : _lastSet;
HashSet<TValue> UpdateAndGetValues()
{
if (_dictionary.TryGetValues(_key, out var valueSet))
{
_lastSet = valueSet._lastSet;

if (_transientReadOnlySet != null)
_transientReadOnlySet.WrappedSet = _lastSet;
}

return _lastSet;
}
}

#region Equality and Hash Code
Expand Down
35 changes: 23 additions & 12 deletions Source/Singulink.Collections/ListDictionary.ReadOnlyValueList.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,22 +52,20 @@ internal ReadOnlyValueList(ListDictionary<TKey, TValue> dictionary, TKey key, Li
/// </summary>
public int Count => GetList().Count;

internal List<TValue> LastList => _lastList;

/// <summary>
/// Returns a fast read-only wrapper around the underlying <see cref="List{T}"/> that is only guaranteed to be valid until the values associated with
/// <see cref="Key"/> in <see cref="Dictionary"/> are modified.
/// Returns a fast read-only view into the underlying list that is only guaranteed to be valid until the values associated with the <see cref="Key"/> in
/// <see cref="Dictionary"/> are modified.
/// </summary>
/// <remarks>
/// <para>References to transient lists should only be held for as long as a series of multiple consequtive read-only operations need to be performed on
/// them. Once the values associated with <see cref="Key"/> are modified, the transient list's behavior is undefined and it may contain stale
/// <para>References to transient lists should only be held for as long as a series of multiple consecutive read-only operations need to be performed on
/// them. Once the values associated with the <see cref="Key"/> are modified, the transient list's behavior is undefined and it may contain stale
/// values.</para>
/// <para>It is not beneficial to use a transient list for a single read operation, but when multipe read operations need to be done consequtively,
/// slight performance gains may be seen from performing them on a transient read-only list instead of directly on a <see cref="ValueList"/> or <see
/// cref="ReadOnlyValueList"/>, since the transient list does not check if it needs to synchronize with a newly attached value list in the
/// dictionary.</para>
/// <para>It is not beneficial to use a transient list for a single read operation, but when multiple read operations need to be done consecutively
/// (including enumerating over multiple values), slight performance gains may be seen from performing them on the transient read-only list instead of
/// directly on the <see cref="ValueList"/> or <see cref="ReadOnlyValueList"/>, since the transient list does not check if it needs to re-synchronize
/// with the dictionary.</para>
/// </remarks>
public ReadOnlyList<TValue> AsTransient()
public ReadOnlyList<TValue> AsTransientReadOnly()
{
var list = GetList();

Expand Down Expand Up @@ -214,13 +212,26 @@ public ReadOnlyList<TValue> AsTransient()
/// </summary>
public Enumerator GetEnumerator() => new(this);

internal List<TValue> LastList => _lastList;

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private protected List<TValue> GetList()
{
return _lastList.Count > 0 ? _lastList : UpdateAndGetValues();

[MethodImpl(MethodImplOptions.NoInlining)]
List<TValue> UpdateAndGetValues() => _dictionary.TryGetValues(_key, out var valueList) ? _lastList = valueList._lastList : _lastList;
List<TValue> UpdateAndGetValues()
{
if (_dictionary.TryGetValues(_key, out var valueList))
{
_lastList = valueList._lastList;

if (_transientReadOnlyList != null)
_transientReadOnlyList.WrappedList = _lastList;
}

return _lastList;
}
}

#region Equality and Hash Code
Expand Down
12 changes: 3 additions & 9 deletions Source/Singulink.Collections/ListDictionary.ValueList.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,8 @@ public sealed class ValueList : ReadOnlyValueList, IList<TValue>, IReadOnlyColle
get => base[index];
set {
var list = GetList();

list[index] = value;

if (index == list.Count)
FinishAdding(list, 1);
else
_dictionary._version++;
_dictionary._version++;
}
}

Expand Down Expand Up @@ -94,8 +89,7 @@ public void Clear()
FinishRemoving(list, removed);
}

#if NET6_0_OR_GREATER

#if NET
/// <summary>
/// Ensures that the capacity of this list is at least the specified capacity.
/// </summary>
Expand All @@ -104,7 +98,7 @@ public int EnsureCapacity(int capacity)
{
// EnsureCapacity causes list version to increment up to net7: https://github.com/dotnet/runtime/issues/82455

if (!Runtime.NET8_OR_HIGHER)
if (!Runtime.IsNet8OrHigher)
_dictionary._version++;

return GetList().EnsureCapacity(capacity);
Expand Down
4 changes: 2 additions & 2 deletions Source/Singulink.Collections/Singulink.Collections.csproj
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netstandard2.0;netstandard2.1;net5.0;net6.0</TargetFrameworks>
<TargetFrameworks>netstandard2.0;netstandard2.1;net6.0</TargetFrameworks>
<NoWarn>CA1034</NoWarn>
<Version>1.0.1</Version>
<Version>2.0</Version>
<Authors>Singulink</Authors>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<Copyright>© Singulink. All rights reserved.</Copyright>
Expand Down
Loading

0 comments on commit 77d039f

Please sign in to comment.