diff --git a/Source/.editorconfig b/.editorconfig similarity index 83% rename from Source/.editorconfig rename to .editorconfig index 1243003..0a1bbce 100644 --- a/Source/.editorconfig +++ b/.editorconfig @@ -9,12 +9,21 @@ root = true guidelines = 160 -########### C# Project files ########### +########### Project XML files ########### [*.csproj] -#### Core EditorConfig Options #### +indent_size = 2 +indent_style = space +tab_width = 2 + +[*.props] + +indent_size = 2 +indent_style = space +tab_width = 2 + +[*.targets] -# Indentation and spacing indent_size = 2 indent_style = space tab_width = 2 @@ -81,10 +90,13 @@ dotnet_code_quality_unused_parameters = all:suggestion #### C# Coding Conventions #### +# Namespace preferences +csharp_style_namespace_declarations = file_scoped:warning + # var preferences csharp_style_var_elsewhere = false:silent -csharp_style_var_for_built_in_types = false:suggestion -csharp_style_var_when_type_is_apparent = true:suggestion +csharp_style_var_for_built_in_types = false:warning +csharp_style_var_when_type_is_apparent = true:warning # Expression-bodied members csharp_style_expression_bodied_accessors = true:suggestion @@ -184,6 +196,14 @@ dotnet_naming_rule.types_should_be_pascalcase.severity = warning dotnet_naming_rule.types_should_be_pascalcase.symbols = types dotnet_naming_rule.types_should_be_pascalcase.style = pascalcase +dotnet_naming_rule.private_static_field_should_be_s_prefixed_camelcase.severity = none +dotnet_naming_rule.private_static_field_should_be_s_prefixed_camelcase.symbols = private_static_field +dotnet_naming_rule.private_static_field_should_be_s_prefixed_camelcase.style = prefixed_camelcase + +dotnet_naming_rule.private_field_should_be_prefixed_camelcase.severity = warning +dotnet_naming_rule.private_field_should_be_prefixed_camelcase.symbols = private_field +dotnet_naming_rule.private_field_should_be_prefixed_camelcase.style = prefixed_camelcase + dotnet_naming_rule.constant_field_should_be_pascalcase.severity = warning dotnet_naming_rule.constant_field_should_be_pascalcase.symbols = constant_field dotnet_naming_rule.constant_field_should_be_pascalcase.style = pascalcase @@ -209,12 +229,25 @@ dotnet_naming_symbols.constant_field.applicable_kinds = field dotnet_naming_symbols.constant_field.applicable_accessibilities = public, internal, private, protected, protected_internal dotnet_naming_symbols.constant_field.required_modifiers = const +dotnet_naming_symbols.private_static_field.applicable_kinds = field +dotnet_naming_symbols.private_static_field.applicable_accessibilities = private +dotnet_naming_symbols.private_static_field.required_modifiers = static + +dotnet_naming_symbols.private_field.applicable_kinds = field +dotnet_naming_symbols.private_field.applicable_accessibilities = private +dotnet_naming_symbols.private_field.required_modifiers = + # Naming styles dotnet_naming_style.pascalcase.required_prefix = dotnet_naming_style.pascalcase.required_suffix = dotnet_naming_style.pascalcase.word_separator = dotnet_naming_style.pascalcase.capitalization = pascal_case +dotnet_naming_style.prefixed_camelcase.required_prefix = _ +dotnet_naming_style.prefixed_camelcase.required_suffix = +dotnet_naming_style.prefixed_camelcase.word_separator = +dotnet_naming_style.prefixed_camelcase.capitalization = camel_case + dotnet_naming_style.begins_with_i.required_prefix = I dotnet_naming_style.begins_with_i.required_suffix = dotnet_naming_style.begins_with_i.word_separator = @@ -346,3 +379,21 @@ dotnet_diagnostic.SA1519.severity = silent # SA1214: Readonly fields should appear before non-readonly fields dotnet_diagnostic.SA1214.severity = suggestion + +# SA1518: Use line endings correctly at end of file +dotnet_diagnostic.SA1518.severity = none + +# SA1402: File may only contain a single type +dotnet_diagnostic.SA1402.severity = none + +# IDE0028: Simplify collection initialization +dotnet_diagnostic.IDE0028.severity = warning + +# IDE0057: Use range operator +dotnet_diagnostic.IDE0057.severity = warning + +# IDE0059: Unnecessary assignment of a value +dotnet_diagnostic.IDE0059.severity = warning + +# RS0030: Do not use banned APIs +dotnet_diagnostic.RS0030.severity = error diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 46500d8..790d362 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -2,11 +2,11 @@ version: 2 updates: - package-ecosystem: nuget target-branch: main - directory: "/Source" + directory: "/" schedule: interval: weekly - package-ecosystem: "github-actions" directory: "/" - schedule: + schedule: interval: weekly target-branch: main \ No newline at end of file diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 4d210c3..b48a9b9 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -19,18 +19,15 @@ jobs: dotnet-version: | 6.0.x 7.0.x + 8.0.x - name: Clean run: dotnet clean --configuration Debug && dotnet nuget locals all --clear - working-directory: Source - name: Install dependencies run: dotnet restore - working-directory: Source - name: Build run: dotnet build --configuration Debug --no-restore - working-directory: Source - name: Test run: dotnet test --configuration Debug --no-build --verbosity normal - working-directory: Source release-windows: @@ -44,15 +41,12 @@ jobs: dotnet-version: | 6.0.x 7.0.x + 8.0.x - name: Clean run: dotnet clean --configuration Release && dotnet nuget locals all --clear - working-directory: Source - name: Install dependencies run: dotnet restore - working-directory: Source - name: Build run: dotnet build --configuration Release --no-restore - working-directory: Source - name: Test - run: dotnet test --configuration Release --no-build --verbosity normal - working-directory: Source \ No newline at end of file + run: dotnet test --configuration Release --no-build --verbosity normal \ No newline at end of file diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 0000000..3d7cee5 --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,30 @@ + + + 12.0 + enable + annotations + CS8769 + enable + true + + + + + + + + + + true + + + + + + + + + + + + \ No newline at end of file diff --git a/LICENSE.md b/LICENSE.md index bf321cb..ab642f8 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,7 +1,7 @@ The MIT License (MIT) -Copyright (c) 2023 +Copyright (c) 2023 Singulink Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/Singulink.Collections.sln b/Singulink.Collections.sln new file mode 100644 index 0000000..efc4ed5 --- /dev/null +++ b/Singulink.Collections.sln @@ -0,0 +1,101 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Singulink.Collections", "Source\Singulink.Collections\Singulink.Collections.csproj", "{1078945F-D4EB-45D7-BD1F-EF33D7A7D308}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Singulink.Collections.Weak", "Source\Singulink.Collections.Weak\Singulink.Collections.Weak.csproj", "{D13CB713-78B9-422B-9BAF-2A35B434FA42}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Singulink.Collections.Tests", "Tests\Singulink.Collections.Tests\Singulink.Collections.Tests.csproj", "{221DC5F0-312A-4FB8-B2FC-D171C921CED6}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Singulink.Collections.Weak.Tests", "Tests\Singulink.Collections.Weak.Tests\Singulink.Collections.Weak.Tests.csproj", "{2F4F3F7C-86C3-473D-90D1-9EFC7147B8D8}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Source", "Source", "{3F3E66F3-E5F4-4243-AB36-583AC6BDDB22}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{D641285C-EB50-4B2E-9FCB-6650DAA65342}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{77D5520C-C625-44CF-9D13-94563173C295}" + ProjectSection(SolutionItems) = preProject + .editorconfig = .editorconfig + CHANGELOG.md = CHANGELOG.md + Directory.Build.props = Directory.Build.props + LICENSE.md = LICENSE.md + README.md = README.md + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Docs", "Docs", "{13264A11-5846-4A14-8E15-0A54AA04BEAB}" + ProjectSection(SolutionItems) = preProject + Docs\docfx.json = Docs\docfx.json + Docs\index.md = Docs\index.md + Docs\toc.yml = Docs\toc.yml + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "api", "api", "{85134FD9-55CA-477B-A8E9-E3F8A6B323F1}" + ProjectSection(SolutionItems) = preProject + Docs\api\index.md = Docs\api\index.md + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".github", ".github", "{4C819D2C-90B2-4490-B5B0-53F1DD9D9EE1}" + ProjectSection(SolutionItems) = preProject + .github\dependabot.yml = .github\dependabot.yml + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Source Items", "Source Items", "{E264B84B-C48C-4D27-8FD2-8C2DE0BAB9B2}" + ProjectSection(SolutionItems) = preProject + Source\Directory.Build.props = Source\Directory.Build.props + Source\Directory.Build.targets = Source\Directory.Build.targets + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Test Items", "Test Items", "{3C98511D-3E04-4D16-B96C-8EFC5216CF4E}" + ProjectSection(SolutionItems) = preProject + Tests\.editorconfig = Tests\.editorconfig + Tests\BannedSymbols.txt = Tests\BannedSymbols.txt + Tests\Directory.Build.props = Tests\Directory.Build.props + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{6371A96E-144C-47C4-BB04-8BC4E6ADA28F}" + ProjectSection(SolutionItems) = preProject + .github\workflows\build-and-test.yml = .github\workflows\build-and-test.yml + EndProjectSection +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {1078945F-D4EB-45D7-BD1F-EF33D7A7D308}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1078945F-D4EB-45D7-BD1F-EF33D7A7D308}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1078945F-D4EB-45D7-BD1F-EF33D7A7D308}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1078945F-D4EB-45D7-BD1F-EF33D7A7D308}.Release|Any CPU.Build.0 = Release|Any CPU + {D13CB713-78B9-422B-9BAF-2A35B434FA42}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D13CB713-78B9-422B-9BAF-2A35B434FA42}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D13CB713-78B9-422B-9BAF-2A35B434FA42}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D13CB713-78B9-422B-9BAF-2A35B434FA42}.Release|Any CPU.Build.0 = Release|Any CPU + {221DC5F0-312A-4FB8-B2FC-D171C921CED6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {221DC5F0-312A-4FB8-B2FC-D171C921CED6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {221DC5F0-312A-4FB8-B2FC-D171C921CED6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {221DC5F0-312A-4FB8-B2FC-D171C921CED6}.Release|Any CPU.Build.0 = Release|Any CPU + {2F4F3F7C-86C3-473D-90D1-9EFC7147B8D8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2F4F3F7C-86C3-473D-90D1-9EFC7147B8D8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2F4F3F7C-86C3-473D-90D1-9EFC7147B8D8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2F4F3F7C-86C3-473D-90D1-9EFC7147B8D8}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {1078945F-D4EB-45D7-BD1F-EF33D7A7D308} = {3F3E66F3-E5F4-4243-AB36-583AC6BDDB22} + {D13CB713-78B9-422B-9BAF-2A35B434FA42} = {3F3E66F3-E5F4-4243-AB36-583AC6BDDB22} + {221DC5F0-312A-4FB8-B2FC-D171C921CED6} = {D641285C-EB50-4B2E-9FCB-6650DAA65342} + {2F4F3F7C-86C3-473D-90D1-9EFC7147B8D8} = {D641285C-EB50-4B2E-9FCB-6650DAA65342} + {85134FD9-55CA-477B-A8E9-E3F8A6B323F1} = {13264A11-5846-4A14-8E15-0A54AA04BEAB} + {E264B84B-C48C-4D27-8FD2-8C2DE0BAB9B2} = {3F3E66F3-E5F4-4243-AB36-583AC6BDDB22} + {3C98511D-3E04-4D16-B96C-8EFC5216CF4E} = {D641285C-EB50-4B2E-9FCB-6650DAA65342} + {6371A96E-144C-47C4-BB04-8BC4E6ADA28F} = {4C819D2C-90B2-4490-B5B0-53F1DD9D9EE1} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {A726AC5A-8E85-4678-ABD0-6746252A4C38} + EndGlobalSection +EndGlobal diff --git a/Source/Directory.Build.props b/Source/Directory.Build.props index 2f115fe..4506251 100644 --- a/Source/Directory.Build.props +++ b/Source/Directory.Build.props @@ -1,31 +1,24 @@ - - 11.0 - enable - enable - 6.0.0 - true - + - - true - + + Singulink + MIT + © Singulink. All rights reserved. + Singulink Icon 128x128.png + - - - + + true + true + true + true + - - - - - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - - - + + + True + + + \ No newline at end of file diff --git a/Source/Directory.Build.targets b/Source/Directory.Build.targets index 6d4f10f..a41ecce 100644 --- a/Source/Directory.Build.targets +++ b/Source/Directory.Build.targets @@ -1,34 +1,34 @@ - - $(BeforePack);AddCompatWarningsToPackage - + + $(BeforePack);AddCompatWarningsToPackage + - - - <_CompatWarningFilePath>$(BaseIntermediateOutputPath)CompatWarning.targets - <_CompatWarningTarget>SLCompatWarning_$(PackageId.Replace('.', '_')) - <_CompatWarningFileContent> - + + + <_CompatWarningFilePath>$(BaseIntermediateOutputPath)CompatWarning.targets + <_CompatWarningTarget>SLCompatWarning_$(PackageId.Replace('.', '_')) + <_CompatWarningFileContent> + ]]> - - + + - + - - + + - - - - + + + + \ No newline at end of file diff --git a/Source/Singulink.Collections.Abstractions/FodyWeavers.xml b/Source/Singulink.Collections.Abstractions/FodyWeavers.xml deleted file mode 100644 index a848a9d..0000000 --- a/Source/Singulink.Collections.Abstractions/FodyWeavers.xml +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/Source/Singulink.Collections.Abstractions/FodyWeavers.xsd b/Source/Singulink.Collections.Abstractions/FodyWeavers.xsd deleted file mode 100644 index 0ceaa88..0000000 --- a/Source/Singulink.Collections.Abstractions/FodyWeavers.xsd +++ /dev/null @@ -1,39 +0,0 @@ - - - - - - - - - - - Determines whether outputs (return values and out/ref parameters) have null checks injected. - - - - - Determines whether non-public members have null checks injected or if only public entry points are checked. - - - - - - - - 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. - - - - - A comma-separated list of error codes that can be safely ignored in assembly verification. - - - - - 'false' to turn off automatic generation of the XML Schema file. - - - - - \ No newline at end of file diff --git a/Source/Singulink.Collections.Abstractions/Singulink.Collections.Abstractions.csproj b/Source/Singulink.Collections.Abstractions/Singulink.Collections.Abstractions.csproj deleted file mode 100644 index 0144e73..0000000 --- a/Source/Singulink.Collections.Abstractions/Singulink.Collections.Abstractions.csproj +++ /dev/null @@ -1,44 +0,0 @@ - - - netstandard2.0;net6.0 - Singulink.Collections - 2.0 - Singulink - MIT - © Singulink. All rights reserved. - https://github.com/Singulink/Singulink.Collections - Singulink Icon 128x128.png - - Collection abstractions for Singulink.Collections. - - Commonly Used Types: - System.Collections.Generic.IReadOnlySet<T> (netstandard polyfill) - Singulink.Collections.ICollectionDictionary<TKey, TValue> - Singulink.Collections.IReadOnlyCollectionDictionary<TKey, TValue> - Singulink.Collections.IMap<TLeft, TRight> - Singulink.Collections.IReadOnlyMap<TLeft, TRight> - - true - key.snk - true - true - - - - true - true - true - true - - - - - - - - - True - - - - \ No newline at end of file diff --git a/Source/Singulink.Collections.Abstractions/key.snk b/Source/Singulink.Collections.Abstractions/key.snk deleted file mode 100644 index befc05f..0000000 Binary files a/Source/Singulink.Collections.Abstractions/key.snk and /dev/null differ diff --git a/Source/Singulink.Collections.Weak/FodyWeavers.xml b/Source/Singulink.Collections.Weak/FodyWeavers.xml deleted file mode 100644 index a848a9d..0000000 --- a/Source/Singulink.Collections.Weak/FodyWeavers.xml +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/Source/Singulink.Collections.Weak/Singulink.Collections.Weak.csproj b/Source/Singulink.Collections.Weak/Singulink.Collections.Weak.csproj index 2d0a6f4..5265463 100644 --- a/Source/Singulink.Collections.Weak/Singulink.Collections.Weak.csproj +++ b/Source/Singulink.Collections.Weak/Singulink.Collections.Weak.csproj @@ -1,40 +1,19 @@ - + netstandard2.0;netstandard2.1;net6.0 Singulink.Collections - Singulink - MIT - Singulink Icon 128x128.png - https://github.com/Singulink/Singulink.Collections.Weak + + 2.0 weak, dictionary, value, collection, list Collection classes that store weak references to values so that they can be garbage collected when they are no longer needed. - © Singulink. All rights reserved. + https://github.com/Singulink/Singulink.Collections.Weak + true key.snk true - 2.0 - - true - true - true - true - - - - - - - - - - True - - - - - - + + diff --git a/Source/Singulink.Collections.Weak/WeakCollection.cs b/Source/Singulink.Collections.Weak/WeakCollection.cs index 0c666a2..71ffdc4 100644 --- a/Source/Singulink.Collections.Weak/WeakCollection.cs +++ b/Source/Singulink.Collections.Weak/WeakCollection.cs @@ -1,204 +1,203 @@ using System.Collections; -namespace Singulink.Collections +namespace Singulink.Collections; + +/// +/// Represents a collection of weakly referenced values that keeps items in an undefined order. If this collection is accessed concurrently from multiple +/// threads (even in a read-only manner) then all accesses must be synchronized with a full lock. +/// +/// +/// On .NET Core 3+, internal entries for garbage collected values are removed as they are encountered (i.e. as they are enumerated over). This is not +/// the case on .NET Standard targets like .NET Framework. You can perform a full clean by calling the method or configure automatic +/// cleaning after a set number of add operations by setting the property. +/// +public sealed class WeakCollection : IEnumerable where T : class { + private readonly HashSet> _entries = []; + + private int _autoCleanAddCount; + private int _addCountSinceLastClean; + /// - /// Represents a collection of weakly referenced values that keeps items in an undefined order. If this collection is accessed concurrently from multiple - /// threads (even in a read-only manner) then all accesses must be synchronized with a full lock. + /// Initializes a new instance of the class. /// - /// - /// On .NET Core 3+, internal entries for garbage collected values are removed as they are encountered (i.e. as they are enumerated over). This is not - /// the case on .NET Standard targets like .NET Framework. You can perform a full clean by calling the method or configure automatic - /// cleaning after a set number of add operations by setting the property. - /// - public sealed class WeakCollection : IEnumerable where T : class + public WeakCollection() { - private readonly HashSet> _entries = new(); + } - private int _autoCleanAddCount; - private int _addCountSinceLastClean; + /// + /// Gets or sets the number of operations that automatically triggers the method to run. Default value is + /// which indicates that automatic cleaning is not performed. + /// + public int? AutoCleanAddCount + { + get => _autoCleanAddCount == 0 ? null : _autoCleanAddCount; + set { + if (value < 1) + throw new ArgumentOutOfRangeException(nameof(value)); - /// - /// Initializes a new instance of the class. - /// - public WeakCollection() - { + _autoCleanAddCount = value.GetValueOrDefault(); } + } - /// - /// Gets or sets the number of operations that automatically triggers the method to run. Default value is - /// which indicates that automatic cleaning is not performed. - /// - public int? AutoCleanAddCount - { - get => _autoCleanAddCount == 0 ? null : _autoCleanAddCount; - set { - if (value < 1) - throw new ArgumentOutOfRangeException(nameof(value)); + /// + /// Gets the number of add operations that have been performed since the last cleaning. + /// + public int AddCountSinceLastClean => _addCountSinceLastClean; - _autoCleanAddCount = value.GetValueOrDefault(); - } - } + /// + /// Gets or sets a value indicating whether to automatically call whenever is called. Default value is + /// . + /// + public bool TrimExcessDuringClean { get; set; } - /// - /// Gets the number of add operations that have been performed since the last cleaning. - /// - public int AddCountSinceLastClean => _addCountSinceLastClean; - - /// - /// Gets or sets a value indicating whether to automatically call whenever is called. Default value is - /// . - /// - public bool TrimExcessDuringClean { get; set; } - - /// - /// Gets the number of entries in the internal data structure. This value will be higher than the actual number of values in the collection if any of - /// the values were garbage collected but still have internal entries in the collection that have not been cleaned. - /// - /// - /// This count will not be accurate if values have been collected since the last clean. You can call to force a full sweep - /// before reading the count to get a more accurate value, but keep in mind that a subsequent enumeration may still return fewer values if they happen - /// to get garbage collected before or during the enumeration. If you require an accurate count together with all the values then you should - /// temporarily copy the values into a strongly referenced collection (like a ) so that they can't be garbage collected and use - /// that to get the count and access the values. - /// - public int UnsafeCount => _entries.Count; - - /// - /// Adds an item to the collection. - /// - public void Add(T item) - { - _entries.Add(new WeakReference(item)); - _addCountSinceLastClean++; + /// + /// Gets the number of entries in the internal data structure. This value will be higher than the actual number of values in the collection if any of + /// the values were garbage collected but still have internal entries in the collection that have not been cleaned. + /// + /// + /// This count will not be accurate if values have been collected since the last clean. You can call to force a full sweep + /// before reading the count to get a more accurate value, but keep in mind that a subsequent enumeration may still return fewer values if they happen + /// to get garbage collected before or during the enumeration. If you require an accurate count together with all the values then you should + /// temporarily copy the values into a strongly referenced collection (like a ) so that they can't be garbage collected and use + /// that to get the count and access the values. + /// + public int UnsafeCount => _entries.Count; - if (_autoCleanAddCount != 0 && _addCountSinceLastClean >= _autoCleanAddCount) - Clean(); - } + /// + /// Adds an item to the collection. + /// + public void Add(T item) + { + _entries.Add(new WeakReference(item)); + _addCountSinceLastClean++; - /// - /// Removes an item from the collection using the specified equality comparer. - /// - /// if the item was removed, otherwise . - public bool Remove(T item, IEqualityComparer? comparer = null) - { - comparer ??= EqualityComparer.Default; + if (_autoCleanAddCount != 0 && _addCountSinceLastClean >= _autoCleanAddCount) + Clean(); + } + + /// + /// Removes an item from the collection using the specified equality comparer. + /// + /// if the item was removed, otherwise . + public bool Remove(T item, IEqualityComparer? comparer = null) + { + comparer ??= EqualityComparer.Default; - foreach (var entry in _entries) + foreach (var entry in _entries) + { + if (entry.TryGetTarget(out var value)) { - if (entry.TryGetTarget(out var value)) - { - if (comparer.Equals(value, item)) - { - _entries.Remove(entry); - return true; - } - } -#if NET - else + if (comparer.Equals(value, item)) { _entries.Remove(entry); + return true; } -#endif } - - return false; +#if NET + else + { + _entries.Remove(entry); + } +#endif } - /// - /// Determines whether the collection contains the given item using the specified equality comparer. - /// - public bool Contains(T item, IEqualityComparer? comparer = null) - { - comparer ??= EqualityComparer.Default; + return false; + } + + /// + /// Determines whether the collection contains the given item using the specified equality comparer. + /// + public bool Contains(T item, IEqualityComparer? comparer = null) + { + comparer ??= EqualityComparer.Default; - foreach (var entry in _entries) + foreach (var entry in _entries) + { + if (entry.TryGetTarget(out var value)) { - if (entry.TryGetTarget(out var value)) - { - if (comparer.Equals(value, item)) - return true; - } + if (comparer.Equals(value, item)) + return true; + } #if NET - else { - _entries.Remove(entry); - } -#endif + else { + _entries.Remove(entry); } - - return false; +#endif } - /// - /// Removes all the elements from the collection. - /// - public void Clear() - { - _entries.Clear(); - _addCountSinceLastClean = 0; - } + return false; + } - /// - /// Removes internal entries for values that have been garbage collected and trims the excess if is set. - /// - public void Clean() - { + /// + /// Removes all the elements from the collection. + /// + public void Clear() + { + _entries.Clear(); + _addCountSinceLastClean = 0; + } + + /// + /// Removes internal entries for values that have been garbage collected and trims the excess if is set. + /// + public void Clean() + { #if NET - var staleEntries = _entries.Where(e => !e.TryGetTarget(out _)); + var staleEntries = _entries.Where(e => !e.TryGetTarget(out _)); #else - var staleEntries = _entries.Where(e => !e.TryGetTarget(out _)).ToList(); + var staleEntries = _entries.Where(e => !e.TryGetTarget(out _)).ToList(); #endif - foreach (var entry in staleEntries) - _entries.Remove(entry); + foreach (var entry in staleEntries) + _entries.Remove(entry); - if (TrimExcessDuringClean) - TrimExcess(); + if (TrimExcessDuringClean) + TrimExcess(); - _addCountSinceLastClean = 0; - } + _addCountSinceLastClean = 0; + } - /// - /// Reduces the internal capacity to the number of entries in the collection. - /// - public void TrimExcess() => _entries.TrimExcess(); - - /// - /// Ensures that this collection can hold the specified number of elements without growing. - /// - /// - /// This method has no effect on .NET Framework. - /// - public void EnsureCapacity(int capacity) - { + /// + /// Reduces the internal capacity to the number of entries in the collection. + /// + public void TrimExcess() => _entries.TrimExcess(); + + /// + /// Ensures that this collection can hold the specified number of elements without growing. + /// + /// + /// This method has no effect on .NET Framework. + /// + public void EnsureCapacity(int capacity) + { #if !NETSTANDARD2_0 - _entries.EnsureCapacity(capacity); + _entries.EnsureCapacity(capacity); #endif - } + } - /// - /// Returns an enumerator that iterates through the collection. - /// - public IEnumerator GetEnumerator() + /// + /// Returns an enumerator that iterates through the collection. + /// + public IEnumerator GetEnumerator() + { + foreach (var entry in _entries) { - foreach (var entry in _entries) - { - if (entry.TryGetTarget(out var item)) - yield return item; + if (entry.TryGetTarget(out var item)) + yield return item; #if NET - else - _entries.Remove(entry); + else + _entries.Remove(entry); #endif - } + } #if NET - _addCountSinceLastClean = 0; + _addCountSinceLastClean = 0; #endif - } - - /// - /// Returns an enumerator that iterates through the collection. - /// - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } + + /// + /// Returns an enumerator that iterates through the collection. + /// + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } \ No newline at end of file diff --git a/Source/Singulink.Collections.Weak/WeakList.cs b/Source/Singulink.Collections.Weak/WeakList.cs index 34274cf..24904f2 100644 --- a/Source/Singulink.Collections.Weak/WeakList.cs +++ b/Source/Singulink.Collections.Weak/WeakList.cs @@ -1,277 +1,276 @@ using System.Collections; -namespace Singulink.Collections +namespace Singulink.Collections; + +/// +/// Represents a collection of weakly referenced values that maintains relative insertion order. If this collection is accessed concurrently from multiple +/// threads in a read-only manner then no locking is necessary, otherwise a full lock or reader/writer lock must be obtained around all accesses. +/// +/// +/// Internal entries for garbage collected values are not removed automatically by default. You can perform a full clean by calling the method or configure automatic cleaning after a set number of operations by setting the property. +/// +public sealed class WeakList : IEnumerable where T : class { + private readonly List> _entries = []; + + private int _autoCleanAddCount; + private int _extraTrimCapacity; + private int _addCountSinceLastClean; + /// - /// Represents a collection of weakly referenced values that maintains relative insertion order. If this collection is accessed concurrently from multiple - /// threads in a read-only manner then no locking is necessary, otherwise a full lock or reader/writer lock must be obtained around all accesses. + /// Initializes a new instance of the class. /// - /// - /// Internal entries for garbage collected values are not removed automatically by default. You can perform a full clean by calling the method or configure automatic cleaning after a set number of operations by setting the property. - /// - public sealed class WeakList : IEnumerable where T : class + public WeakList() { - private readonly List> _entries = new List>(); + } - private int _autoCleanAddCount; - private int _extraTrimCapacity; - private int _addCountSinceLastClean; + /// + /// Gets or sets the number of operations that automatically triggers the method to run. Default value is + /// which indicates that automatic cleaning is not performed. + /// + public int? AutoCleanAddCount + { + get => _autoCleanAddCount == 0 ? null : _autoCleanAddCount; + set { + if (value < 1) + throw new ArgumentOutOfRangeException(nameof(value)); - /// - /// Initializes a new instance of the class. - /// - public WeakList() - { + _autoCleanAddCount = value.GetValueOrDefault(); } + } - /// - /// Gets or sets the number of operations that automatically triggers the method to run. Default value is - /// which indicates that automatic cleaning is not performed. - /// - public int? AutoCleanAddCount - { - get => _autoCleanAddCount == 0 ? null : _autoCleanAddCount; - set { - if (value < 1) - throw new ArgumentOutOfRangeException(nameof(value)); - - _autoCleanAddCount = value.GetValueOrDefault(); - } - } + /// + /// Gets the number of add operations that have been performed since the last cleaning. + /// + public int AddCountSinceLastClean => _addCountSinceLastClean; - /// - /// Gets the number of add operations that have been performed since the last cleaning. - /// - public int AddCountSinceLastClean => _addCountSinceLastClean; - - /// - /// Gets or sets a value indicating whether to automatically call whenever is called. Default value is - /// . - /// - public bool TrimExcessDuringClean { get; set; } - - /// - /// Gets or sets the extra capacity to leave when is called. Default value is 0. - /// - public int ExtraTrimCapacity - { - get => _extraTrimCapacity; - set { - if (_extraTrimCapacity < 0) - throw new ArgumentOutOfRangeException(nameof(value)); + /// + /// Gets or sets a value indicating whether to automatically call whenever is called. Default value is + /// . + /// + public bool TrimExcessDuringClean { get; set; } - _extraTrimCapacity = value; - } - } + /// + /// Gets or sets the extra capacity to leave when is called. Default value is 0. + /// + public int ExtraTrimCapacity + { + get => _extraTrimCapacity; + set { + if (_extraTrimCapacity < 0) + throw new ArgumentOutOfRangeException(nameof(value)); - /// - /// Gets the number of entries in the internal data structure. This value will be different than the actual count if any of the values were garbage - /// collected but still have internal entries in the list that have not been cleaned. - /// - /// - /// This count will not be accurate if values have been collected since the last clean. You can call to force a full sweep - /// before reading the count to get a more accurate value, but keep in mind that a subsequent enumeration may still return fewer values if they happen - /// to get garbage collected before or during the enumeration. If you require an accurate count together with all the values then you should - /// temporarily copy the values into a strongly referenced collection (like a ) so that they can't be garbage collected and use - /// that to get the count and access the values. - /// - public int UnsafeCount => _entries.Count; - - /// - /// Gets or sets the total number of elements the internal data structure can hold without resizing. - /// - public int Capacity - { - get => _entries.Capacity; - set => _entries.Capacity = value; + _extraTrimCapacity = value; } + } - /// - /// Adds an item to the end of the collection. - /// - public void Add(T item) - { - _entries.Add(new WeakReference(item)); - OnAdded(); - } + /// + /// Gets the number of entries in the internal data structure. This value will be different than the actual count if any of the values were garbage + /// collected but still have internal entries in the list that have not been cleaned. + /// + /// + /// This count will not be accurate if values have been collected since the last clean. You can call to force a full sweep + /// before reading the count to get a more accurate value, but keep in mind that a subsequent enumeration may still return fewer values if they happen + /// to get garbage collected before or during the enumeration. If you require an accurate count together with all the values then you should + /// temporarily copy the values into a strongly referenced collection (like a ) so that they can't be garbage collected and use + /// that to get the count and access the values. + /// + public int UnsafeCount => _entries.Count; - /// - /// Inserts an item to the beginning of the collection. - /// - public void InsertFirst(T item) - { - _entries.Insert(0, new WeakReference(item)); - OnAdded(); - } + /// + /// Gets or sets the total number of elements the internal data structure can hold without resizing. + /// + public int Capacity + { + get => _entries.Capacity; + set => _entries.Capacity = value; + } - /// - /// Inserts an item before another item. - /// - /// The item to find to determine the insertion point. - /// The item to insert. - /// The comparer to use to determine item equality. - /// The item to insert before was not found. - public void InsertBefore(T findItem, T item, IEqualityComparer? comparer = null) - { - if (!TryInsertBefore(findItem, item, comparer)) - throw new ArgumentException("The specified item was not found.", nameof(findItem)); - } + /// + /// Adds an item to the end of the collection. + /// + public void Add(T item) + { + _entries.Add(new WeakReference(item)); + OnAdded(); + } - /// - /// Inserts an item before another item. - /// - /// The item to find to determine the insertion point. - /// The item to insert. - /// The comparer to use to determine item equality. - /// if the item to insert after was found and the item was inserted, otherwise . - public bool TryInsertBefore(T findItem, T item, IEqualityComparer? comparer = null) - { - comparer ??= EqualityComparer.Default; + /// + /// Inserts an item to the beginning of the collection. + /// + public void InsertFirst(T item) + { + _entries.Insert(0, new WeakReference(item)); + OnAdded(); + } - for (int i = 0; i < _entries.Count; i++) - { - if (_entries[i].TryGetTarget(out var currentItem) && comparer.Equals(currentItem, findItem)) - { - _entries.Insert(i, new WeakReference(item)); - OnAdded(); - return true; - } - } + /// + /// Inserts an item before another item. + /// + /// The item to find to determine the insertion point. + /// The item to insert. + /// The comparer to use to determine item equality. + /// The item to insert before was not found. + public void InsertBefore(T findItem, T item, IEqualityComparer? comparer = null) + { + if (!TryInsertBefore(findItem, item, comparer)) + throw new ArgumentException("The specified item was not found.", nameof(findItem)); + } - return false; - } + /// + /// Inserts an item before another item. + /// + /// The item to find to determine the insertion point. + /// The item to insert. + /// The comparer to use to determine item equality. + /// if the item to insert after was found and the item was inserted, otherwise . + public bool TryInsertBefore(T findItem, T item, IEqualityComparer? comparer = null) + { + comparer ??= EqualityComparer.Default; - /// - /// Inserts an item after another item. - /// - /// The item to find to determine the insertion point. - /// The item to insert. - /// The comparer to use to determine item equality. - /// The item to insert after was not found. - public void InsertAfter(T findItem, T item, IEqualityComparer? comparer = null) + for (int i = 0; i < _entries.Count; i++) { - if (!TryInsertAfter(findItem, item, comparer)) - throw new ArgumentException("The specified item was not found.", nameof(findItem)); + if (_entries[i].TryGetTarget(out var currentItem) && comparer.Equals(currentItem, findItem)) + { + _entries.Insert(i, new WeakReference(item)); + OnAdded(); + return true; + } } - /// - /// Inserts an item after another item. - /// - /// The item to find to determine the insertion point. - /// The item to insert. - /// The comparer to use to determine item equality. - /// if the item to insert after was found and the item was inserted, otherwise . - public bool TryInsertAfter(T findItem, T item, IEqualityComparer? comparer = null) - { - comparer ??= EqualityComparer.Default; + return false; + } - for (int i = 0; i < _entries.Count; i++) - { - if (_entries[i].TryGetTarget(out var currentItem) && comparer.Equals(currentItem, findItem)) - { - _entries.Insert(i + 1, new WeakReference(item)); - OnAdded(); - return true; - } - } + /// + /// Inserts an item after another item. + /// + /// The item to find to determine the insertion point. + /// The item to insert. + /// The comparer to use to determine item equality. + /// The item to insert after was not found. + public void InsertAfter(T findItem, T item, IEqualityComparer? comparer = null) + { + if (!TryInsertAfter(findItem, item, comparer)) + throw new ArgumentException("The specified item was not found.", nameof(findItem)); + } - return false; - } + /// + /// Inserts an item after another item. + /// + /// The item to find to determine the insertion point. + /// The item to insert. + /// The comparer to use to determine item equality. + /// if the item to insert after was found and the item was inserted, otherwise . + public bool TryInsertAfter(T findItem, T item, IEqualityComparer? comparer = null) + { + comparer ??= EqualityComparer.Default; - /// - /// Removes an item from the collection using the specified equality comparer. - /// - /// if the item was removed, otherwise . - public bool Remove(T item, IEqualityComparer? comparer = null) + for (int i = 0; i < _entries.Count; i++) { - comparer ??= EqualityComparer.Default; - - for (int i = 0; i < _entries.Count; i++) + if (_entries[i].TryGetTarget(out var currentItem) && comparer.Equals(currentItem, findItem)) { - if (_entries[i].TryGetTarget(out var currentItem) && comparer.Equals(currentItem, item)) - { - _entries.RemoveAt(i); - return true; - } + _entries.Insert(i + 1, new WeakReference(item)); + OnAdded(); + return true; } - - return false; } - /// - /// Determines whether the collection contains the given item using the specified equality comparer. - /// - public bool Contains(T item, IEqualityComparer? comparer = null) - { - comparer ??= EqualityComparer.Default; + return false; + } + + /// + /// Removes an item from the collection using the specified equality comparer. + /// + /// if the item was removed, otherwise . + public bool Remove(T item, IEqualityComparer? comparer = null) + { + comparer ??= EqualityComparer.Default; - foreach (var entry in _entries) + for (int i = 0; i < _entries.Count; i++) + { + if (_entries[i].TryGetTarget(out var currentItem) && comparer.Equals(currentItem, item)) { - if (entry.TryGetTarget(out var currentItem) && comparer.Equals(currentItem, item)) - return true; + _entries.RemoveAt(i); + return true; } - - return false; } - /// - /// Removes all the elements from the collection. - /// - public void Clear() + return false; + } + + /// + /// Determines whether the collection contains the given item using the specified equality comparer. + /// + public bool Contains(T item, IEqualityComparer? comparer = null) + { + comparer ??= EqualityComparer.Default; + + foreach (var entry in _entries) { - _entries.Clear(); - _addCountSinceLastClean = 0; + if (entry.TryGetTarget(out var currentItem) && comparer.Equals(currentItem, item)) + return true; } - /// - /// Removes internal entries for values that have been garbage collected and trims the excess if is set. - /// - public void Clean() - { - _entries.RemoveAll(entry => !entry.TryGetTarget(out _)); + return false; + } - if (TrimExcessDuringClean) - TrimExcess(); + /// + /// Removes all the elements from the collection. + /// + public void Clear() + { + _entries.Clear(); + _addCountSinceLastClean = 0; + } - _addCountSinceLastClean = 0; - } + /// + /// Removes internal entries for values that have been garbage collected and trims the excess if is set. + /// + public void Clean() + { + _entries.RemoveAll(entry => !entry.TryGetTarget(out _)); - /// - /// Reduces the internal capacity to the number of entries in the collection plus . - /// - public void TrimExcess() - { - int trimmedCapacity = _entries.Count + ExtraTrimCapacity; + if (TrimExcessDuringClean) + TrimExcess(); - if (trimmedCapacity < _entries.Capacity) - _entries.Capacity = trimmedCapacity; - } + _addCountSinceLastClean = 0; + } - /// - /// Returns an enumerator that iterates through the collection. - /// - public IEnumerator GetEnumerator() - { - foreach (var entry in _entries) - { - if (entry.TryGetTarget(out var item)) - yield return item; - } - } + /// + /// Reduces the internal capacity to the number of entries in the collection plus . + /// + public void TrimExcess() + { + int trimmedCapacity = _entries.Count + ExtraTrimCapacity; - private void OnAdded() - { - _addCountSinceLastClean++; + if (trimmedCapacity < _entries.Capacity) + _entries.Capacity = trimmedCapacity; + } - if (_autoCleanAddCount != 0 && _addCountSinceLastClean >= _autoCleanAddCount) - Clean(); + /// + /// Returns an enumerator that iterates through the collection. + /// + public IEnumerator GetEnumerator() + { + foreach (var entry in _entries) + { + if (entry.TryGetTarget(out var item)) + yield return item; } + } + + private void OnAdded() + { + _addCountSinceLastClean++; - /// - /// Returns an enumerator that iterates through the collection. - /// - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + if (_autoCleanAddCount != 0 && _addCountSinceLastClean >= _autoCleanAddCount) + Clean(); } + + /// + /// Returns an enumerator that iterates through the collection. + /// + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } \ No newline at end of file diff --git a/Source/Singulink.Collections.Weak/WeakValueDictionary.cs b/Source/Singulink.Collections.Weak/WeakValueDictionary.cs index 77747ac..317c774 100644 --- a/Source/Singulink.Collections.Weak/WeakValueDictionary.cs +++ b/Source/Singulink.Collections.Weak/WeakValueDictionary.cs @@ -1,337 +1,336 @@ using System.Collections; using System.Diagnostics.CodeAnalysis; -namespace Singulink.Collections +namespace Singulink.Collections; + +/// +/// Represents a collection of keys and weakly referenced values. If this collection is accessed concurrently from multiple threads (even in a read-only +/// manner) then all accesses must be synchronized with a full lock. +/// +/// +/// On .NET, 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 .NET Framework. You can perform a full clean by calling the +/// method or configure automatic cleaning after a set number of add operations by setting the property. +/// +public class WeakValueDictionary : IEnumerable> + where TKey : notnull + where TValue : class { - /// - /// Represents a collection of keys and weakly referenced values. If this collection is accessed concurrently from multiple threads (even in a read-only - /// manner) then all accesses must be synchronized with a full lock. - /// - /// - /// On .NET, 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 .NET Framework. You can perform a full clean by calling the - /// method or configure automatic cleaning after a set number of add operations by setting the property. - /// - public class WeakValueDictionary : IEnumerable> - where TKey : notnull - where TValue : class - { #if NETSTANDARD2_0 - private Dictionary> _entryLookup; - private int _capacity; + private Dictionary> _entryLookup; + private int _capacity; #else - private readonly Dictionary> _entryLookup; + private readonly Dictionary> _entryLookup; #endif - private int _autoCleanAddCount; - private int _addCountSinceLastClean; + private int _autoCleanAddCount; + private int _addCountSinceLastClean; - /// - /// Initializes a new instance of the class. - /// - public WeakValueDictionary() : this(null) - { - } + /// + /// Initializes a new instance of the class. + /// + public WeakValueDictionary() : this(null) + { + } - /// - /// Initializes a new instance of the class using the specified key equality comparer. - /// - public WeakValueDictionary(IEqualityComparer? comparer) - { - _entryLookup = new(comparer); - } + /// + /// Initializes a new instance of the class using the specified key equality comparer. + /// + public WeakValueDictionary(IEqualityComparer? comparer) + { + _entryLookup = new(comparer); + } - /// - /// Gets or sets the number of add (or indexer set) operations that automatically triggers the method to run. Default value is - /// which indicates that automatic cleaning is not performed. - /// - public int? AutoCleanAddCount - { - get => _autoCleanAddCount == 0 ? null : _autoCleanAddCount; - set { - if (value < 1) - throw new ArgumentOutOfRangeException(nameof(value)); + /// + /// Gets or sets the number of add (or indexer set) operations that automatically triggers the method to run. Default value is + /// which indicates that automatic cleaning is not performed. + /// + public int? AutoCleanAddCount + { + get => _autoCleanAddCount == 0 ? null : _autoCleanAddCount; + set { + if (value < 1) + throw new ArgumentOutOfRangeException(nameof(value)); - _autoCleanAddCount = value.GetValueOrDefault(); - } + _autoCleanAddCount = value.GetValueOrDefault(); } + } - /// - /// Gets the number of add (or indexer set) operations that have been performed since the last cleaning. - /// - public int AddCountSinceLastClean => _addCountSinceLastClean; - - /// - /// Gets or sets a value indicating whether to automatically call whenever is called. Default value is - /// . - /// - public bool TrimExcessDuringClean { get; set; } - - /// - /// Gets the keys in the dictionary. - /// - public IEnumerable Keys => this.Select(kvp => kvp.Key); - - /// - /// Gets the values in the dictionary. - /// - public IEnumerable Values => this.Select(kvp => kvp.Value); - - /// - /// Gets the number of entries in the internal data structure. This value will be different than the actual count if any of the values were garbage - /// collected but still have internal entries in the dictionary that have not been cleaned. - /// - /// - /// This count will not be accurate if values have been collected since the last clean. You can call to force a full sweep - /// before reading the count to get a more accurate value, but keep in mind that a subsequent enumeration may still return fewer values if they happen - /// to get garbage collected before or during the enumeration. If you require an accurate count together with all the values then you should - /// temporarily copy the values into a strongly referenced collection (like a or ) so that - /// they can't be garbage collected and use that to get the count and access the values. - /// - public int UnsafeCount => _entryLookup.Count; - - /// - /// Gets or sets the value associated with the specified key. - /// - public TValue this[TKey key] - { - get { - if (!TryGetValue(key, out var value)) - throw new KeyNotFoundException(); + /// + /// Gets the number of add (or indexer set) operations that have been performed since the last cleaning. + /// + public int AddCountSinceLastClean => _addCountSinceLastClean; - return value; - } - set { - _entryLookup[key] = new WeakReference(value); - OnAdded(); - } - } + /// + /// Gets or sets a value indicating whether to automatically call whenever is called. Default value is + /// . + /// + public bool TrimExcessDuringClean { get; set; } - /// - /// Gets the value associated with the specified key. - /// - /// The key of the value to get. - /// The value associated with the specified key, otherwise . - /// if the dictionary contains a value with the specified key, otherwise . - public bool TryGetValue(TKey key, [NotNullWhen(true)] out TValue? value) - { - if (_entryLookup.TryGetValue(key, out var entry)) - { - if (entry.TryGetTarget(out value)) - return true; -#if NET - else - _entryLookup.Remove(key); -#endif - } + /// + /// Gets the keys in the dictionary. + /// + public IEnumerable Keys => this.Select(kvp => kvp.Key); - value = null; - return false; - } + /// + /// Gets the values in the dictionary. + /// + public IEnumerable Values => this.Select(kvp => kvp.Value); - /// - /// Adds the specified key and value to the dictionary. - /// - public bool TryAdd(TKey key, TValue value) - { - if (_entryLookup.TryGetValue(key, out var entry)) - { - if (entry.TryGetTarget(out var _)) - return false; + /// + /// Gets the number of entries in the internal data structure. This value will be different than the actual count if any of the values were garbage + /// collected but still have internal entries in the dictionary that have not been cleaned. + /// + /// + /// This count will not be accurate if values have been collected since the last clean. You can call to force a full sweep + /// before reading the count to get a more accurate value, but keep in mind that a subsequent enumeration may still return fewer values if they happen + /// to get garbage collected before or during the enumeration. If you require an accurate count together with all the values then you should + /// temporarily copy the values into a strongly referenced collection (like a or ) so that + /// they can't be garbage collected and use that to get the count and access the values. + /// + public int UnsafeCount => _entryLookup.Count; - entry.SetTarget(value); - } - else - { - _entryLookup.Add(key, new WeakReference(value)); - } + /// + /// Gets or sets the value associated with the specified key. + /// + public TValue this[TKey key] + { + get { + if (!TryGetValue(key, out var value)) + throw new KeyNotFoundException(); + return value; + } + set { + _entryLookup[key] = new WeakReference(value); OnAdded(); - return true; } + } - /// - /// Adds the specified key and value to the dictionary. - /// - /// The specified key already exists in the dictionary. - public void Add(TKey key, TValue value) + /// + /// Gets the value associated with the specified key. + /// + /// The key of the value to get. + /// The value associated with the specified key, otherwise . + /// if the dictionary contains a value with the specified key, otherwise . + public bool TryGetValue(TKey key, [NotNullWhen(true)] out TValue? value) + { + if (_entryLookup.TryGetValue(key, out var entry)) { - if (!TryAdd(key, value)) - throw new ArgumentException("Specified key already exists.", nameof(key)); + if (entry.TryGetTarget(out value)) + return true; +#if NET + else + _entryLookup.Remove(key); +#endif } - /// - /// Removes the value with the specified key from the dictionary. - /// - /// if the item was found and removed, otherwise . - public bool Remove(TKey key) + value = null; + return false; + } + + /// + /// Adds the specified key and value to the dictionary. + /// + public bool TryAdd(TKey key, TValue value) + { + if (_entryLookup.TryGetValue(key, out var entry)) { - if (_entryLookup.TryGetValue(key, out var entry)) - { - _entryLookup.Remove(key); - return entry.TryGetTarget(out _); - } + if (entry.TryGetTarget(out var _)) + return false; - return false; + entry.SetTarget(value); + } + else + { + _entryLookup.Add(key, new WeakReference(value)); } - /// - /// Removes the value with the specified key from the dictionary. - /// - /// The key of the value to remove. - /// The value that was removed, otherwise . - /// if the item was found and removed, otherwise . - public bool Remove(TKey key, [MaybeNullWhen(false)] out TValue value) + OnAdded(); + return true; + } + + /// + /// Adds the specified key and value to the dictionary. + /// + /// The specified key already exists in the dictionary. + public void Add(TKey key, TValue value) + { + if (!TryAdd(key, value)) + throw new ArgumentException("Specified key already exists.", nameof(key)); + } + + /// + /// Removes the value with the specified key from the dictionary. + /// + /// if the item was found and removed, otherwise . + public bool Remove(TKey key) + { + if (_entryLookup.TryGetValue(key, out var entry)) { - if (_entryLookup.TryGetValue(key, out var entry)) - { - _entryLookup.Remove(key); - return entry.TryGetTarget(out value); - } + _entryLookup.Remove(key); + return entry.TryGetTarget(out _); + } + + return false; + } - value = default; - return false; + /// + /// Removes the value with the specified key from the dictionary. + /// + /// The key of the value to remove. + /// The value that was removed, otherwise . + /// if the item was found and removed, otherwise . + public bool Remove(TKey key, [MaybeNullWhen(false)] out TValue value) + { + if (_entryLookup.TryGetValue(key, out var entry)) + { + _entryLookup.Remove(key); + return entry.TryGetTarget(out value); } - /// - /// Removes the entry with the given key and value from the dictionary using the specified equality comparer for the value type. - /// - public bool Remove(TKey key, TValue value, IEqualityComparer? comparer = null) + value = default; + return false; + } + + /// + /// Removes the entry with the given key and value from the dictionary using the specified equality comparer for the value type. + /// + public bool Remove(TKey key, TValue value, IEqualityComparer? comparer = null) + { + if (_entryLookup.TryGetValue(key, out var entry)) { - if (_entryLookup.TryGetValue(key, out var entry)) + if (entry.TryGetTarget(out var current)) { - if (entry.TryGetTarget(out var current)) - { - if ((comparer ?? EqualityComparer.Default).Equals(value, current)) - { - _entryLookup.Remove(key); - return true; - } - } - else + if ((comparer ?? EqualityComparer.Default).Equals(value, current)) { _entryLookup.Remove(key); + return true; } } - - return false; + else + { + _entryLookup.Remove(key); + } } - /// - /// Indictes whether the dictionary contains the specified key/value pair using the optionally specified value comparer. - /// - public bool Contains(KeyValuePair kvp, IEqualityComparer? comparer = null) => Contains(kvp.Key, kvp.Value, comparer); + return false; + } - /// - /// Indictes whether the dictionary contains the key and value using the optionally specified value comparer. - /// - public bool Contains(TKey key, TValue value, IEqualityComparer? comparer = null) - { - return TryGetValue(key, out var current) && (comparer ?? EqualityComparer.Default).Equals(value, current); - } + /// + /// Indictes whether the dictionary contains the specified key/value pair using the optionally specified value comparer. + /// + public bool Contains(KeyValuePair kvp, IEqualityComparer? comparer = null) => Contains(kvp.Key, kvp.Value, comparer); - /// - /// Determines whether the dictionary contains the specified key. - /// - public bool ContainsKey(TKey key) => TryGetValue(key, out _); + /// + /// Indictes whether the dictionary contains the key and value using the optionally specified value comparer. + /// + public bool Contains(TKey key, TValue value, IEqualityComparer? comparer = null) + { + return TryGetValue(key, out var current) && (comparer ?? EqualityComparer.Default).Equals(value, current); + } - /// - /// Determines whether the dictionary contains the specified value. - /// - public bool ContainsValue(TValue value, IEqualityComparer? comparer = null) => Values.Contains(value, comparer); + /// + /// Determines whether the dictionary contains the specified key. + /// + public bool ContainsKey(TKey key) => TryGetValue(key, out _); - /// - /// Removes all keys and values from the dictionary. - /// - public void Clear() - { - _entryLookup.Clear(); - _addCountSinceLastClean = 0; - } + /// + /// Determines whether the dictionary contains the specified value. + /// + public bool ContainsValue(TValue value, IEqualityComparer? comparer = null) => Values.Contains(value, comparer); - /// - /// Removes internal entries that refer to values that have been garbage collected. - /// - public void Clean() - { + /// + /// Removes all keys and values from the dictionary. + /// + public void Clear() + { + _entryLookup.Clear(); + _addCountSinceLastClean = 0; + } + + /// + /// Removes internal entries that refer to values that have been garbage collected. + /// + public void Clean() + { #if NET - var staleKvps = _entryLookup.Where(kvp => !kvp.Value.TryGetTarget(out _)); + var staleKvps = _entryLookup.Where(kvp => !kvp.Value.TryGetTarget(out _)); #else - var staleKvps = _entryLookup.Where(kvp => !kvp.Value.TryGetTarget(out _)).ToList(); + var staleKvps = _entryLookup.Where(kvp => !kvp.Value.TryGetTarget(out _)).ToList(); #endif - foreach (var kvp in staleKvps) - _entryLookup.Remove(kvp.Key); + foreach (var kvp in staleKvps) + _entryLookup.Remove(kvp.Key); - if (TrimExcessDuringClean) - TrimExcess(); + if (TrimExcessDuringClean) + TrimExcess(); - _addCountSinceLastClean = 0; - } + _addCountSinceLastClean = 0; + } - /// - /// Reduces the internal capacity of this dictionary to the size needed to hold the current entries. - /// - public void TrimExcess() - { + /// + /// Reduces the internal capacity of this dictionary to the size needed to hold the current entries. + /// + public void TrimExcess() + { #if NETSTANDARD2_0 - if (_capacity > _entryLookup.Count * 2) - { - _entryLookup = new Dictionary>(_entryLookup, _entryLookup.Comparer); - _capacity = _entryLookup.Count; - } + if (_capacity > _entryLookup.Count * 2) + { + _entryLookup = new Dictionary>(_entryLookup, _entryLookup.Comparer); + _capacity = _entryLookup.Count; + } #else - _entryLookup.TrimExcess(); + _entryLookup.TrimExcess(); #endif - } + } - /// - /// Ensures that this dictionary can hold the specified number of elements without growing. - /// - /// - /// This method has no effect on .NET Framework. - /// - public void EnsureCapacity(int capacity) - { + /// + /// Ensures that this dictionary can hold the specified number of elements without growing. + /// + /// + /// This method has no effect on .NET Framework. + /// + public void EnsureCapacity(int capacity) + { #if !NETSTANDARD2_0 - _entryLookup.EnsureCapacity(capacity); + _entryLookup.EnsureCapacity(capacity); #endif - } + } - /// - /// Returns an enumerator that iterates through the key/value pairs in the dictionary. - /// - public IEnumerator> GetEnumerator() + /// + /// Returns an enumerator that iterates through the key/value pairs in the dictionary. + /// + public IEnumerator> GetEnumerator() + { + foreach (var kvp in _entryLookup) { - foreach (var kvp in _entryLookup) - { - if (kvp.Value.TryGetTarget(out var value)) - yield return new KeyValuePair(kvp.Key, value); + if (kvp.Value.TryGetTarget(out var value)) + yield return new KeyValuePair(kvp.Key, value); #if NET - else - _entryLookup.Remove(kvp.Key); + else + _entryLookup.Remove(kvp.Key); #endif - } + } #if NET - _addCountSinceLastClean = 0; + _addCountSinceLastClean = 0; #endif - } + } - private void OnAdded() - { + private void OnAdded() + { #if NETSTANDARD2_0 - if (_entryLookup.Count > _capacity) - _capacity = _entryLookup.Count; + if (_entryLookup.Count > _capacity) + _capacity = _entryLookup.Count; #endif - _addCountSinceLastClean++; + _addCountSinceLastClean++; - if (_autoCleanAddCount != 0 && _addCountSinceLastClean >= _autoCleanAddCount) - Clean(); - } - - /// - /// Returns an enumerator that iterates through the key/value pairs in the dictionary. - /// - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + if (_autoCleanAddCount != 0 && _addCountSinceLastClean >= _autoCleanAddCount) + Clean(); } + + /// + /// Returns an enumerator that iterates through the key/value pairs in the dictionary. + /// + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } \ No newline at end of file diff --git a/Source/Singulink.Collections.Weak/stylecop.json b/Source/Singulink.Collections.Weak/stylecop.json deleted file mode 100644 index d4d297b..0000000 --- a/Source/Singulink.Collections.Weak/stylecop.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - // Enabling configuration: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/EnableConfiguration.md - - "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json", - "settings": { - "documentationRules": { - "documentExposedElements": true, - "documentInternalElements": false, - "documentInterfaces": false - } - } -} diff --git a/Source/Singulink.Collections.sln b/Source/Singulink.Collections.sln deleted file mode 100644 index ace6c46..0000000 --- a/Source/Singulink.Collections.sln +++ /dev/null @@ -1,115 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.4.33103.184 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Singulink.Collections", "Singulink.Collections\Singulink.Collections.csproj", "{C802E214-0FE3-4115-A4E9-B08C8EE76759}" -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 - Directory.Build.targets = Directory.Build.targets - ..\README.md = ..\README.md - EndProjectSection -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Singulink.Collections.Abstractions", "Singulink.Collections.Abstractions\Singulink.Collections.Abstractions.csproj", "{8EF2D844-0A43-47EF-B055-030D13F02088}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Singulink.Collections.Tests", "Tests\Singulink.Collections.Tests\Singulink.Collections.Tests.csproj", "{37173589-E411-4806-A79C-153D9659FAE7}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Test Items", "Test Items", "{465CA120-1038-480C-893C-AE32B93017A3}" - ProjectSection(SolutionItems) = preProject - Tests\.editorconfig = Tests\.editorconfig - Tests\Directory.Build.props = Tests\Directory.Build.props - EndProjectSection -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{B95CC845-E2D3-4EF5-B7DF-FC902D58FF4C}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Singulink.Collections.Weak", "Singulink.Collections.Weak\Singulink.Collections.Weak.csproj", "{5EDDA4AB-D81A-4FC2-9C71-D25B8A8B13F1}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Singulink.Collections.Weak.Tests", "Tests\Singulink.Collections.Weak.Tests\Singulink.Collections.Weak.Tests.csproj", "{9534B47A-355D-4769-AA16-A5BE4511600C}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Debug|x64 = Debug|x64 - Debug|x86 = Debug|x86 - Release|Any CPU = Release|Any CPU - Release|x64 = Release|x64 - Release|x86 = Release|x86 - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {C802E214-0FE3-4115-A4E9-B08C8EE76759}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C802E214-0FE3-4115-A4E9-B08C8EE76759}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C802E214-0FE3-4115-A4E9-B08C8EE76759}.Debug|x64.ActiveCfg = Debug|Any CPU - {C802E214-0FE3-4115-A4E9-B08C8EE76759}.Debug|x64.Build.0 = Debug|Any CPU - {C802E214-0FE3-4115-A4E9-B08C8EE76759}.Debug|x86.ActiveCfg = Debug|Any CPU - {C802E214-0FE3-4115-A4E9-B08C8EE76759}.Debug|x86.Build.0 = Debug|Any CPU - {C802E214-0FE3-4115-A4E9-B08C8EE76759}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C802E214-0FE3-4115-A4E9-B08C8EE76759}.Release|Any CPU.Build.0 = Release|Any CPU - {C802E214-0FE3-4115-A4E9-B08C8EE76759}.Release|x64.ActiveCfg = Release|Any CPU - {C802E214-0FE3-4115-A4E9-B08C8EE76759}.Release|x64.Build.0 = Release|Any CPU - {C802E214-0FE3-4115-A4E9-B08C8EE76759}.Release|x86.ActiveCfg = Release|Any CPU - {C802E214-0FE3-4115-A4E9-B08C8EE76759}.Release|x86.Build.0 = Release|Any CPU - {8EF2D844-0A43-47EF-B055-030D13F02088}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {8EF2D844-0A43-47EF-B055-030D13F02088}.Debug|Any CPU.Build.0 = Debug|Any CPU - {8EF2D844-0A43-47EF-B055-030D13F02088}.Debug|x64.ActiveCfg = Debug|Any CPU - {8EF2D844-0A43-47EF-B055-030D13F02088}.Debug|x64.Build.0 = Debug|Any CPU - {8EF2D844-0A43-47EF-B055-030D13F02088}.Debug|x86.ActiveCfg = Debug|Any CPU - {8EF2D844-0A43-47EF-B055-030D13F02088}.Debug|x86.Build.0 = Debug|Any CPU - {8EF2D844-0A43-47EF-B055-030D13F02088}.Release|Any CPU.ActiveCfg = Release|Any CPU - {8EF2D844-0A43-47EF-B055-030D13F02088}.Release|Any CPU.Build.0 = Release|Any CPU - {8EF2D844-0A43-47EF-B055-030D13F02088}.Release|x64.ActiveCfg = Release|Any CPU - {8EF2D844-0A43-47EF-B055-030D13F02088}.Release|x64.Build.0 = Release|Any CPU - {8EF2D844-0A43-47EF-B055-030D13F02088}.Release|x86.ActiveCfg = Release|Any CPU - {8EF2D844-0A43-47EF-B055-030D13F02088}.Release|x86.Build.0 = Release|Any CPU - {37173589-E411-4806-A79C-153D9659FAE7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {37173589-E411-4806-A79C-153D9659FAE7}.Debug|Any CPU.Build.0 = Debug|Any CPU - {37173589-E411-4806-A79C-153D9659FAE7}.Debug|x64.ActiveCfg = Debug|Any CPU - {37173589-E411-4806-A79C-153D9659FAE7}.Debug|x64.Build.0 = Debug|Any CPU - {37173589-E411-4806-A79C-153D9659FAE7}.Debug|x86.ActiveCfg = Debug|Any CPU - {37173589-E411-4806-A79C-153D9659FAE7}.Debug|x86.Build.0 = Debug|Any CPU - {37173589-E411-4806-A79C-153D9659FAE7}.Release|Any CPU.ActiveCfg = Release|Any CPU - {37173589-E411-4806-A79C-153D9659FAE7}.Release|Any CPU.Build.0 = Release|Any CPU - {37173589-E411-4806-A79C-153D9659FAE7}.Release|x64.ActiveCfg = Release|Any CPU - {37173589-E411-4806-A79C-153D9659FAE7}.Release|x64.Build.0 = Release|Any CPU - {37173589-E411-4806-A79C-153D9659FAE7}.Release|x86.ActiveCfg = Release|Any CPU - {37173589-E411-4806-A79C-153D9659FAE7}.Release|x86.Build.0 = Release|Any CPU - {5EDDA4AB-D81A-4FC2-9C71-D25B8A8B13F1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {5EDDA4AB-D81A-4FC2-9C71-D25B8A8B13F1}.Debug|Any CPU.Build.0 = Debug|Any CPU - {5EDDA4AB-D81A-4FC2-9C71-D25B8A8B13F1}.Debug|x64.ActiveCfg = Debug|Any CPU - {5EDDA4AB-D81A-4FC2-9C71-D25B8A8B13F1}.Debug|x64.Build.0 = Debug|Any CPU - {5EDDA4AB-D81A-4FC2-9C71-D25B8A8B13F1}.Debug|x86.ActiveCfg = Debug|Any CPU - {5EDDA4AB-D81A-4FC2-9C71-D25B8A8B13F1}.Debug|x86.Build.0 = Debug|Any CPU - {5EDDA4AB-D81A-4FC2-9C71-D25B8A8B13F1}.Release|Any CPU.ActiveCfg = Release|Any CPU - {5EDDA4AB-D81A-4FC2-9C71-D25B8A8B13F1}.Release|Any CPU.Build.0 = Release|Any CPU - {5EDDA4AB-D81A-4FC2-9C71-D25B8A8B13F1}.Release|x64.ActiveCfg = Release|Any CPU - {5EDDA4AB-D81A-4FC2-9C71-D25B8A8B13F1}.Release|x64.Build.0 = Release|Any CPU - {5EDDA4AB-D81A-4FC2-9C71-D25B8A8B13F1}.Release|x86.ActiveCfg = Release|Any CPU - {5EDDA4AB-D81A-4FC2-9C71-D25B8A8B13F1}.Release|x86.Build.0 = Release|Any CPU - {9534B47A-355D-4769-AA16-A5BE4511600C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {9534B47A-355D-4769-AA16-A5BE4511600C}.Debug|Any CPU.Build.0 = Debug|Any CPU - {9534B47A-355D-4769-AA16-A5BE4511600C}.Debug|x64.ActiveCfg = Debug|Any CPU - {9534B47A-355D-4769-AA16-A5BE4511600C}.Debug|x64.Build.0 = Debug|Any CPU - {9534B47A-355D-4769-AA16-A5BE4511600C}.Debug|x86.ActiveCfg = Debug|Any CPU - {9534B47A-355D-4769-AA16-A5BE4511600C}.Debug|x86.Build.0 = Debug|Any CPU - {9534B47A-355D-4769-AA16-A5BE4511600C}.Release|Any CPU.ActiveCfg = Release|Any CPU - {9534B47A-355D-4769-AA16-A5BE4511600C}.Release|Any CPU.Build.0 = Release|Any CPU - {9534B47A-355D-4769-AA16-A5BE4511600C}.Release|x64.ActiveCfg = Release|Any CPU - {9534B47A-355D-4769-AA16-A5BE4511600C}.Release|x64.Build.0 = Release|Any CPU - {9534B47A-355D-4769-AA16-A5BE4511600C}.Release|x86.ActiveCfg = Release|Any CPU - {9534B47A-355D-4769-AA16-A5BE4511600C}.Release|x86.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(NestedProjects) = preSolution - {37173589-E411-4806-A79C-153D9659FAE7} = {B95CC845-E2D3-4EF5-B7DF-FC902D58FF4C} - {465CA120-1038-480C-893C-AE32B93017A3} = {B95CC845-E2D3-4EF5-B7DF-FC902D58FF4C} - {9534B47A-355D-4769-AA16-A5BE4511600C} = {B95CC845-E2D3-4EF5-B7DF-FC902D58FF4C} - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {62EF1A76-6B44-49F1-A73B-520A855AEEBA} - EndGlobalSection -EndGlobal diff --git a/Source/Singulink.Collections/FodyWeavers.xml b/Source/Singulink.Collections/FodyWeavers.xml deleted file mode 100644 index a848a9d..0000000 --- a/Source/Singulink.Collections/FodyWeavers.xml +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/Source/Singulink.Collections/HashSetDictionary.cs b/Source/Singulink.Collections/HashSetDictionary.cs index b931d3d..25ed542 100644 --- a/Source/Singulink.Collections/HashSetDictionary.cs +++ b/Source/Singulink.Collections/HashSetDictionary.cs @@ -197,7 +197,7 @@ public bool ContainsValue(TValue value) /// Ensures that the dictionary can hold up to a specified number of key/value set pairs without any further expansion of its backing storage. /// /// The number of key/value set pairs. - /// The currect capacity of the dictionary. + /// The current capacity of the dictionary. /// Capacity specified is less than 0. public int EnsureCapacity(int capacity) { diff --git a/Source/Singulink.Collections.Abstractions/ICollectionDictionary.cs b/Source/Singulink.Collections/ICollectionDictionary.cs similarity index 100% rename from Source/Singulink.Collections.Abstractions/ICollectionDictionary.cs rename to Source/Singulink.Collections/ICollectionDictionary.cs diff --git a/Source/Singulink.Collections.Abstractions/IListDictionary.cs b/Source/Singulink.Collections/IListDictionary.cs similarity index 100% rename from Source/Singulink.Collections.Abstractions/IListDictionary.cs rename to Source/Singulink.Collections/IListDictionary.cs diff --git a/Source/Singulink.Collections.Abstractions/IMap.cs b/Source/Singulink.Collections/IMap.cs similarity index 100% rename from Source/Singulink.Collections.Abstractions/IMap.cs rename to Source/Singulink.Collections/IMap.cs diff --git a/Source/Singulink.Collections.Abstractions/IReadOnlyCollectionDictionary.cs b/Source/Singulink.Collections/IReadOnlyCollectionDictionary.cs similarity index 100% rename from Source/Singulink.Collections.Abstractions/IReadOnlyCollectionDictionary.cs rename to Source/Singulink.Collections/IReadOnlyCollectionDictionary.cs diff --git a/Source/Singulink.Collections.Abstractions/IReadOnlyCollectionProvider.cs b/Source/Singulink.Collections/IReadOnlyCollectionProvider.cs similarity index 82% rename from Source/Singulink.Collections.Abstractions/IReadOnlyCollectionProvider.cs rename to Source/Singulink.Collections/IReadOnlyCollectionProvider.cs index bb70775..085eea2 100644 --- a/Source/Singulink.Collections.Abstractions/IReadOnlyCollectionProvider.cs +++ b/Source/Singulink.Collections/IReadOnlyCollectionProvider.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace Singulink.Collections; +namespace Singulink.Collections; /// /// Allows a collection to return a read-only wrapper. This interface should be implemented by the value collections in collection dictionaries to diff --git a/Source/Singulink.Collections.Abstractions/IReadOnlyListDictionary.cs b/Source/Singulink.Collections/IReadOnlyListDictionary.cs similarity index 100% rename from Source/Singulink.Collections.Abstractions/IReadOnlyListDictionary.cs rename to Source/Singulink.Collections/IReadOnlyListDictionary.cs diff --git a/Source/Singulink.Collections.Abstractions/IReadOnlyMap.cs b/Source/Singulink.Collections/IReadOnlyMap.cs similarity index 100% rename from Source/Singulink.Collections.Abstractions/IReadOnlyMap.cs rename to Source/Singulink.Collections/IReadOnlyMap.cs diff --git a/Source/Singulink.Collections.Abstractions/IReadOnlySet.cs b/Source/Singulink.Collections/IReadOnlySet.cs similarity index 100% rename from Source/Singulink.Collections.Abstractions/IReadOnlySet.cs rename to Source/Singulink.Collections/IReadOnlySet.cs diff --git a/Source/Singulink.Collections.Abstractions/IReadOnlySetDictionary.cs b/Source/Singulink.Collections/IReadOnlySetDictionary.cs similarity index 100% rename from Source/Singulink.Collections.Abstractions/IReadOnlySetDictionary.cs rename to Source/Singulink.Collections/IReadOnlySetDictionary.cs diff --git a/Source/Singulink.Collections.Abstractions/ISetDictionary.cs b/Source/Singulink.Collections/ISetDictionary.cs similarity index 100% rename from Source/Singulink.Collections.Abstractions/ISetDictionary.cs rename to Source/Singulink.Collections/ISetDictionary.cs diff --git a/Source/Singulink.Collections/Polyfills/CollectionExtensions.cs b/Source/Singulink.Collections/Polyfills/CollectionExtensions.cs index 809879b..2cdbd8d 100644 --- a/Source/Singulink.Collections/Polyfills/CollectionExtensions.cs +++ b/Source/Singulink.Collections/Polyfills/CollectionExtensions.cs @@ -1,12 +1,10 @@ -using System.Diagnostics.CodeAnalysis; - -namespace Singulink.Collections; +namespace Singulink.Collections; #if NETSTANDARD2_0 internal static class CollectionExtensions { - public static bool Remove(this Dictionary dictionary, TKey key, [MaybeNullWhen(false)] out TValue value) where TKey : notnull + public static bool Remove(this Dictionary dictionary, TKey key, out TValue value) where TKey : notnull { if (!dictionary.TryGetValue(key, out value)) return false; diff --git a/Source/Singulink.Collections/Singulink.Collections.csproj b/Source/Singulink.Collections/Singulink.Collections.csproj index a46f954..521a6e8 100644 --- a/Source/Singulink.Collections/Singulink.Collections.csproj +++ b/Source/Singulink.Collections/Singulink.Collections.csproj @@ -1,13 +1,10 @@  netstandard2.0;netstandard2.1;net6.0 - CA1034 + $(NoWarn);CA1034 + 2.0 - Singulink - MIT - © Singulink. All rights reserved. - https://github.com/Singulink/Singulink.Collections - Singulink Icon 128x128.png + List; Dictionary; HashSet; Set Widely useful highly optimized collections that are missing from the .NET BCL. @@ -18,32 +15,14 @@ Singulink.Collections.ReadOnlyList<T> Singulink.Collections.ReadOnlyHashSet<T> + https://github.com/Singulink/Singulink.Collections + true key.snk true - List; Dictionary; HashSet; Set - e9d5dddc-d4fb-4a06-84bd-190ee6180360 - - true - true - true - true - - - - - - - - - True - - - - - - + + diff --git a/Source/Singulink.Collections/Utilities/CollectionCopy.cs b/Source/Singulink.Collections/Utilities/CollectionCopy.cs index a6070dc..f335c20 100644 --- a/Source/Singulink.Collections/Utilities/CollectionCopy.cs +++ b/Source/Singulink.Collections/Utilities/CollectionCopy.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; - -namespace Singulink.Collections.Utilities; +namespace Singulink.Collections.Utilities; internal static class CollectionCopy { diff --git a/Source/Singulink.Collections/Utilities/Throw.cs b/Source/Singulink.Collections/Utilities/Throw.cs index 19ab542..ce348c8 100644 --- a/Source/Singulink.Collections/Utilities/Throw.cs +++ b/Source/Singulink.Collections/Utilities/Throw.cs @@ -1,6 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.CodeAnalysis; namespace Singulink.Collections.Utilities; diff --git a/Source/Tests/Directory.Build.props b/Source/Tests/Directory.Build.props deleted file mode 100644 index 28dadc6..0000000 --- a/Source/Tests/Directory.Build.props +++ /dev/null @@ -1,7 +0,0 @@ - - - - CS1591 - false - - \ No newline at end of file diff --git a/Source/Tests/Singulink.Collections.Tests/FodyWeavers.xml b/Source/Tests/Singulink.Collections.Tests/FodyWeavers.xml deleted file mode 100644 index a848a9d..0000000 --- a/Source/Tests/Singulink.Collections.Tests/FodyWeavers.xml +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/Source/Tests/Singulink.Collections.Tests/FodyWeavers.xsd b/Source/Tests/Singulink.Collections.Tests/FodyWeavers.xsd deleted file mode 100644 index 0ceaa88..0000000 --- a/Source/Tests/Singulink.Collections.Tests/FodyWeavers.xsd +++ /dev/null @@ -1,39 +0,0 @@ - - - - - - - - - - - Determines whether outputs (return values and out/ref parameters) have null checks injected. - - - - - Determines whether non-public members have null checks injected or if only public entry points are checked. - - - - - - - - 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. - - - - - A comma-separated list of error codes that can be safely ignored in assembly verification. - - - - - 'false' to turn off automatic generation of the XML Schema file. - - - - - \ No newline at end of file diff --git a/Source/Tests/Singulink.Collections.Tests/Singulink.Collections.Tests.csproj b/Source/Tests/Singulink.Collections.Tests/Singulink.Collections.Tests.csproj deleted file mode 100644 index ad08681..0000000 --- a/Source/Tests/Singulink.Collections.Tests/Singulink.Collections.Tests.csproj +++ /dev/null @@ -1,20 +0,0 @@ - - - net48;net6.0 - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - \ No newline at end of file diff --git a/Source/Tests/Singulink.Collections.Weak.Tests/FodyWeavers.xml b/Source/Tests/Singulink.Collections.Weak.Tests/FodyWeavers.xml deleted file mode 100644 index a848a9d..0000000 --- a/Source/Tests/Singulink.Collections.Weak.Tests/FodyWeavers.xml +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/Source/Tests/Singulink.Collections.Weak.Tests/FodyWeavers.xsd b/Source/Tests/Singulink.Collections.Weak.Tests/FodyWeavers.xsd deleted file mode 100644 index 0ceaa88..0000000 --- a/Source/Tests/Singulink.Collections.Weak.Tests/FodyWeavers.xsd +++ /dev/null @@ -1,39 +0,0 @@ - - - - - - - - - - - Determines whether outputs (return values and out/ref parameters) have null checks injected. - - - - - Determines whether non-public members have null checks injected or if only public entry points are checked. - - - - - - - - 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. - - - - - A comma-separated list of error codes that can be safely ignored in assembly verification. - - - - - 'false' to turn off automatic generation of the XML Schema file. - - - - - \ No newline at end of file diff --git a/Source/Tests/Singulink.Collections.Weak.Tests/GCTests.cs b/Source/Tests/Singulink.Collections.Weak.Tests/GCTests.cs deleted file mode 100644 index 6d34ddd..0000000 --- a/Source/Tests/Singulink.Collections.Weak.Tests/GCTests.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace Singulink.Collections.Weak.Tests -{ - [TestClass] - public class GCTests - { - [TestMethod] - public void EnterNoGCRegionAndCollect() - { - var weakRef = Helpers.GetWeakRef(); - - using (NoGCRegion.Enter(1000)) { } - - Helpers.CollectAndWait(); - Assert.IsFalse(weakRef.TryGetTarget(out _)); - } - } -} \ No newline at end of file diff --git a/Source/Tests/Singulink.Collections.Weak.Tests/Helpers.cs b/Source/Tests/Singulink.Collections.Weak.Tests/Helpers.cs deleted file mode 100644 index 8f18a2e..0000000 --- a/Source/Tests/Singulink.Collections.Weak.Tests/Helpers.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System.Runtime.CompilerServices; - -namespace Singulink.Collections.Weak.Tests -{ - internal static class Helpers - { - [MethodImpl(MethodImplOptions.NoInlining)] - public static WeakReference GetWeakRef() - { - return new WeakReference(new object()); - } - - public static void CollectAndWait() - { - GC.Collect(); - GC.WaitForPendingFinalizers(); - GC.Collect(); - } - } -} \ No newline at end of file diff --git a/Source/Tests/Singulink.Collections.Weak.Tests/NoGCRegion.cs b/Source/Tests/Singulink.Collections.Weak.Tests/NoGCRegion.cs deleted file mode 100644 index ed3442a..0000000 --- a/Source/Tests/Singulink.Collections.Weak.Tests/NoGCRegion.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; - -#pragma warning disable CA1815 // Override equals and operator equals on value types - -namespace Singulink.Collections.Weak.Tests -{ - public struct NoGCRegion : IDisposable - { - public static NoGCRegion Enter(long memoryNeeded) - { - Assert.AreEqual(true, GC.TryStartNoGCRegion(memoryNeeded)); - return default; - } - - public void Dispose() - { - GC.EndNoGCRegion(); - } - } -} \ No newline at end of file diff --git a/Source/Tests/Singulink.Collections.Weak.Tests/Singulink.Collections.Weak.Tests.csproj b/Source/Tests/Singulink.Collections.Weak.Tests/Singulink.Collections.Weak.Tests.csproj deleted file mode 100644 index 76d3c32..0000000 --- a/Source/Tests/Singulink.Collections.Weak.Tests/Singulink.Collections.Weak.Tests.csproj +++ /dev/null @@ -1,19 +0,0 @@ - - - net48;net6.0 - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - diff --git a/Source/Tests/Singulink.Collections.Weak.Tests/WeakCollectionTests.cs b/Source/Tests/Singulink.Collections.Weak.Tests/WeakCollectionTests.cs deleted file mode 100644 index 460d73a..0000000 --- a/Source/Tests/Singulink.Collections.Weak.Tests/WeakCollectionTests.cs +++ /dev/null @@ -1,80 +0,0 @@ -using System.Runtime.CompilerServices; -using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace Singulink.Collections.Weak.Tests -{ - [TestClass] - public class WeakCollectionTests - { - [TestMethod] - public void Clean() - { - var c = new WeakCollection(); - object x = new object(); - - using (NoGCRegion.Enter(1000)) - { - c.Add(x); - c.Add(x); - c.Add(x); - - AddCollectableItems(c, 3); - - Assert.AreEqual(6, c.AddCountSinceLastClean); - Assert.AreEqual(6, c.UnsafeCount); - } - - Helpers.CollectAndWait(); - - c.Clean(); - Assert.AreEqual(0, c.AddCountSinceLastClean); - Assert.AreEqual(3, c.UnsafeCount); - - GC.KeepAlive(x); - } - - [TestMethod] - public void EnumerationCleaning() - { - var c = new WeakCollection(); - object x = new object(); - - using (NoGCRegion.Enter(1000)) - { - c.Add(x); - c.Add(x); - c.Add(x); - - AddCollectableItems(c, 3); - - Assert.AreEqual(6, c.AddCountSinceLastClean); - Assert.AreEqual(6, c.UnsafeCount); - } - - Helpers.CollectAndWait(); - - foreach (object o in c) - { } - - Assert.IsTrue(c.Remove(x)); - Assert.IsTrue(c.Remove(x)); - -#if NET48 // NS2.0 target does not support removing stale entries as items are encountered. - Assert.AreEqual(6, c.AddCountSinceLastClean); - Assert.AreEqual(4, c.UnsafeCount); -#else - Assert.AreEqual(0, c.AddCountSinceLastClean); - Assert.AreEqual(1, c.UnsafeCount); -#endif - - GC.KeepAlive(x); - } - - [MethodImpl(MethodImplOptions.NoInlining)] - private static void AddCollectableItems(WeakCollection c, int count) - { - for (int i = 0; i < count; i++) - c.Add(new object()); - } - } -} \ No newline at end of file diff --git a/Source/Tests/Singulink.Collections.Weak.Tests/WeakListTests.cs b/Source/Tests/Singulink.Collections.Weak.Tests/WeakListTests.cs deleted file mode 100644 index 71df29d..0000000 --- a/Source/Tests/Singulink.Collections.Weak.Tests/WeakListTests.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System; -using System.Linq; -using System.Runtime.CompilerServices; -using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace Singulink.Collections.Weak.Tests -{ - [TestClass] - public class WeakListTests - { - [TestMethod] - public void Clean() - { - var c = new WeakList(); - object x = new object(); - - using (NoGCRegion.Enter(1000)) { - AddCollectableItems(c, 3); - - c.InsertFirst(x); - c.InsertAfter(x, x); - c.InsertBefore(x, x); - - Assert.AreEqual(6, c.AddCountSinceLastClean); - Assert.AreEqual(6, c.UnsafeCount); - Assert.IsTrue(c.Take(3).SequenceEqual(new[] { x, x, x })); - } - - Helpers.CollectAndWait(); - - c.Clean(); - Assert.AreEqual(0, c.AddCountSinceLastClean); - Assert.AreEqual(3, c.UnsafeCount); - - c.Remove(x); - c.Remove(x); - Assert.AreEqual(1, c.UnsafeCount); - - GC.KeepAlive(x); - } - - [MethodImpl(MethodImplOptions.NoInlining)] - private static void AddCollectableItems(WeakList c, int count) - { - for (int i = 0; i < count; i++) - c.Add(new object()); - } - } -} \ No newline at end of file diff --git a/Source/Tests/Singulink.Collections.Weak.Tests/WeakValueDictionaryTests.cs b/Source/Tests/Singulink.Collections.Weak.Tests/WeakValueDictionaryTests.cs deleted file mode 100644 index bb14d2c..0000000 --- a/Source/Tests/Singulink.Collections.Weak.Tests/WeakValueDictionaryTests.cs +++ /dev/null @@ -1,88 +0,0 @@ -using System.Runtime.CompilerServices; -using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace Singulink.Collections.Weak.Tests -{ - [TestClass] - public class WeakValueDictionaryTests - { - [TestMethod] - public void Clean() - { - var c = new WeakValueDictionary(); - object x = new(); - - using (NoGCRegion.Enter(1000)) - { - c.Add(0, x); - c.Add(1, x); - c.Add(2, x); - - AddCollectableItems(c, 3, 3); - - Assert.AreEqual(6, c.AddCountSinceLastClean); - Assert.AreEqual(6, c.UnsafeCount); - } - - Helpers.CollectAndWait(); - - c.Clean(); - Assert.AreEqual(0, c.AddCountSinceLastClean); - Assert.AreEqual(3, c.UnsafeCount); - - GC.KeepAlive(x); - } - - [TestMethod] - public void EnumerationCleaning() - { - var c = new WeakValueDictionary(); - object x = new(); - - using (NoGCRegion.Enter(1000)) - { - c.Add(0, x); - c.Add(1, x); - c.Add(2, x); - - AddCollectableItems(c, 3, 3); - - Assert.AreEqual(6, c.AddCountSinceLastClean); - Assert.AreEqual(6, c.UnsafeCount); - Assert.IsTrue(c.ContainsKey(1)); - -#if DEBUG || !NETFRAMEWORK // Causes entry with key 4 not to collect on NETFW release builds - Assert.IsTrue(c.ContainsKey(4)); -#endif - } - - Helpers.CollectAndWait(); - - Assert.IsTrue(c.ContainsKey(1)); - Assert.IsFalse(c.ContainsKey(4)); - - foreach (object o in c) { } - - Assert.IsTrue(c.Remove(0)); - Assert.IsTrue(c.Remove(1)); - Assert.IsFalse(c.Remove(4)); - -#if NET48 // NS2.0 target does not support removing stale entries as items are encountered. - Assert.AreEqual(6, c.AddCountSinceLastClean); - Assert.AreEqual(3, c.UnsafeCount); -#else - Assert.AreEqual(0, c.AddCountSinceLastClean); - Assert.AreEqual(1, c.UnsafeCount); -#endif - - GC.KeepAlive(x); - } - - [MethodImpl(MethodImplOptions.NoInlining)] - private static void AddCollectableItems(WeakValueDictionary c, int startKey, int count) - { - for (int i = 0; i < count; i++) - c.Add(startKey++, new object()); - } - } -} \ No newline at end of file diff --git a/Source/Tests/.editorconfig b/Tests/.editorconfig similarity index 100% rename from Source/Tests/.editorconfig rename to Tests/.editorconfig diff --git a/Tests/BannedSymbols.txt b/Tests/BannedSymbols.txt new file mode 100644 index 0000000..9c7161c --- /dev/null +++ b/Tests/BannedSymbols.txt @@ -0,0 +1,2 @@ +T:Microsoft.VisualStudio.TestTools.UnitTesting.TestClassAttribute; Use [PrefixTestClass] instead. +T:Microsoft.VisualStudio.TestTools.UnitTesting.Assert; Use Shouldly instead. \ No newline at end of file diff --git a/Tests/Directory.Build.props b/Tests/Directory.Build.props new file mode 100644 index 0000000..f62df65 --- /dev/null +++ b/Tests/Directory.Build.props @@ -0,0 +1,20 @@ + + + + + CS1591 + false + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Source/Tests/Singulink.Collections.Tests/HashSetDictionary/AsReadOnlyTests.cs b/Tests/Singulink.Collections.Tests/HashSetDictionary/AsReadOnlyTests.cs similarity index 96% rename from Source/Tests/Singulink.Collections.Tests/HashSetDictionary/AsReadOnlyTests.cs rename to Tests/Singulink.Collections.Tests/HashSetDictionary/AsReadOnlyTests.cs index 8613614..9f13476 100644 --- a/Source/Tests/Singulink.Collections.Tests/HashSetDictionary/AsReadOnlyTests.cs +++ b/Tests/Singulink.Collections.Tests/HashSetDictionary/AsReadOnlyTests.cs @@ -1,6 +1,6 @@ namespace Singulink.Collections.Tests.HashSetDictionary; -[TestClass] +[PrefixTestClass] public class AsReadOnlyTests { [TestMethod] diff --git a/Source/Tests/Singulink.Collections.Tests/HashSetDictionary/FlowTests.cs b/Tests/Singulink.Collections.Tests/HashSetDictionary/FlowTests.cs similarity index 96% rename from Source/Tests/Singulink.Collections.Tests/HashSetDictionary/FlowTests.cs rename to Tests/Singulink.Collections.Tests/HashSetDictionary/FlowTests.cs index cc7d4eb..614ab99 100644 --- a/Source/Tests/Singulink.Collections.Tests/HashSetDictionary/FlowTests.cs +++ b/Tests/Singulink.Collections.Tests/HashSetDictionary/FlowTests.cs @@ -1,6 +1,6 @@ namespace Singulink.Collections.Tests.HashSetDictionary; -[TestClass] +[PrefixTestClass] public class FlowTests { [TestMethod] diff --git a/Source/Tests/Singulink.Collections.Tests/ListDictionary/AsReadOnlyTests.cs b/Tests/Singulink.Collections.Tests/ListDictionary/AsReadOnlyTests.cs similarity index 96% rename from Source/Tests/Singulink.Collections.Tests/ListDictionary/AsReadOnlyTests.cs rename to Tests/Singulink.Collections.Tests/ListDictionary/AsReadOnlyTests.cs index 8925a8c..834a33e 100644 --- a/Source/Tests/Singulink.Collections.Tests/ListDictionary/AsReadOnlyTests.cs +++ b/Tests/Singulink.Collections.Tests/ListDictionary/AsReadOnlyTests.cs @@ -1,6 +1,6 @@ namespace Singulink.Collections.Tests.ListDictionary; -[TestClass] +[PrefixTestClass] public class AsReadOnlyTests { [TestMethod] diff --git a/Source/Tests/Singulink.Collections.Tests/ListDictionary/FlowTests.cs b/Tests/Singulink.Collections.Tests/ListDictionary/FlowTests.cs similarity index 95% rename from Source/Tests/Singulink.Collections.Tests/ListDictionary/FlowTests.cs rename to Tests/Singulink.Collections.Tests/ListDictionary/FlowTests.cs index 7cccd31..03357cd 100644 --- a/Source/Tests/Singulink.Collections.Tests/ListDictionary/FlowTests.cs +++ b/Tests/Singulink.Collections.Tests/ListDictionary/FlowTests.cs @@ -1,6 +1,6 @@ namespace Singulink.Collections.Tests.ListDictionary; -[TestClass] +[PrefixTestClass] public class FlowTests { [TestMethod] diff --git a/Source/Tests/Singulink.Collections.Tests/Map/FlowTests.cs b/Tests/Singulink.Collections.Tests/Map/FlowTests.cs similarity index 96% rename from Source/Tests/Singulink.Collections.Tests/Map/FlowTests.cs rename to Tests/Singulink.Collections.Tests/Map/FlowTests.cs index 420e038..7bba961 100644 --- a/Source/Tests/Singulink.Collections.Tests/Map/FlowTests.cs +++ b/Tests/Singulink.Collections.Tests/Map/FlowTests.cs @@ -1,6 +1,6 @@ namespace Singulink.Collections.Tests.Map; -[TestClass] +[PrefixTestClass] public class FlowTests { [TestMethod] diff --git a/Tests/Singulink.Collections.Tests/Singulink.Collections.Tests.csproj b/Tests/Singulink.Collections.Tests/Singulink.Collections.Tests.csproj new file mode 100644 index 0000000..efc2e3d --- /dev/null +++ b/Tests/Singulink.Collections.Tests/Singulink.Collections.Tests.csproj @@ -0,0 +1,9 @@ + + + net48;net6.0 + + + + + + \ No newline at end of file diff --git a/Source/Tests/Singulink.Collections.Tests/Usings.cs b/Tests/Singulink.Collections.Tests/Usings.cs similarity index 67% rename from Source/Tests/Singulink.Collections.Tests/Usings.cs rename to Tests/Singulink.Collections.Tests/Usings.cs index d327e13..da6fe16 100644 --- a/Source/Tests/Singulink.Collections.Tests/Usings.cs +++ b/Tests/Singulink.Collections.Tests/Usings.cs @@ -1,2 +1,3 @@ global using Microsoft.VisualStudio.TestTools.UnitTesting; +global using PrefixClassName.MsTest; global using Shouldly; \ No newline at end of file diff --git a/Tests/Singulink.Collections.Weak.Tests/GCTests.cs b/Tests/Singulink.Collections.Weak.Tests/GCTests.cs new file mode 100644 index 0000000..c2debdc --- /dev/null +++ b/Tests/Singulink.Collections.Weak.Tests/GCTests.cs @@ -0,0 +1,16 @@ +namespace Singulink.Collections.Weak.Tests; + +[PrefixTestClass] +public class GCTests +{ + [TestMethod] + public void EnterNoGCRegionAndCollect() + { + var weakRef = Helpers.GetWeakRef(); + + using (NoGCRegion.Enter(10000)) { } + + Helpers.CollectAndWait(); + weakRef.TryGetTarget(out _).ShouldBeFalse(); + } +} \ No newline at end of file diff --git a/Tests/Singulink.Collections.Weak.Tests/Helpers.cs b/Tests/Singulink.Collections.Weak.Tests/Helpers.cs new file mode 100644 index 0000000..40cedb3 --- /dev/null +++ b/Tests/Singulink.Collections.Weak.Tests/Helpers.cs @@ -0,0 +1,19 @@ +using System.Runtime.CompilerServices; + +namespace Singulink.Collections.Weak.Tests; + +internal static class Helpers +{ + [MethodImpl(MethodImplOptions.NoInlining)] + public static WeakReference GetWeakRef() + { + return new WeakReference(new object()); + } + + public static void CollectAndWait() + { + GC.Collect(); + GC.WaitForPendingFinalizers(); + GC.Collect(); + } +} \ No newline at end of file diff --git a/Tests/Singulink.Collections.Weak.Tests/NoGCRegion.cs b/Tests/Singulink.Collections.Weak.Tests/NoGCRegion.cs new file mode 100644 index 0000000..1d8604b --- /dev/null +++ b/Tests/Singulink.Collections.Weak.Tests/NoGCRegion.cs @@ -0,0 +1,17 @@ +#pragma warning disable CA1815 // Override equals and operator equals on value types + +namespace Singulink.Collections.Weak.Tests; + +public struct NoGCRegion : IDisposable +{ + public static NoGCRegion Enter(long memoryNeeded) + { + GC.TryStartNoGCRegion(memoryNeeded).ShouldBeTrue(); + return default; + } + + public void Dispose() + { + GC.EndNoGCRegion(); + } +} \ No newline at end of file diff --git a/Tests/Singulink.Collections.Weak.Tests/Singulink.Collections.Weak.Tests.csproj b/Tests/Singulink.Collections.Weak.Tests/Singulink.Collections.Weak.Tests.csproj new file mode 100644 index 0000000..3b2d827 --- /dev/null +++ b/Tests/Singulink.Collections.Weak.Tests/Singulink.Collections.Weak.Tests.csproj @@ -0,0 +1,9 @@ + + + net48;net6.0 + + + + + + diff --git a/Tests/Singulink.Collections.Weak.Tests/Usings.cs b/Tests/Singulink.Collections.Weak.Tests/Usings.cs new file mode 100644 index 0000000..da6fe16 --- /dev/null +++ b/Tests/Singulink.Collections.Weak.Tests/Usings.cs @@ -0,0 +1,3 @@ +global using Microsoft.VisualStudio.TestTools.UnitTesting; +global using PrefixClassName.MsTest; +global using Shouldly; \ No newline at end of file diff --git a/Tests/Singulink.Collections.Weak.Tests/WeakCollectionTests.cs b/Tests/Singulink.Collections.Weak.Tests/WeakCollectionTests.cs new file mode 100644 index 0000000..274d286 --- /dev/null +++ b/Tests/Singulink.Collections.Weak.Tests/WeakCollectionTests.cs @@ -0,0 +1,90 @@ +using System.Runtime.CompilerServices; + +namespace Singulink.Collections.Weak.Tests; + +[PrefixTestClass] +public class WeakCollectionTests +{ + [TestMethod] + public void Clean() + { + var c = new WeakCollection(); + object x = new(); + + int noGcAddCountSinceLastClean; + int noGcUnsafeCount; + + using (NoGCRegion.Enter(10000)) + { + c.Add(x); + c.Add(x); + c.Add(x); + + AddCollectableItems(c, 3); + + noGcAddCountSinceLastClean = c.AddCountSinceLastClean; + noGcUnsafeCount = c.UnsafeCount; + } + + noGcAddCountSinceLastClean.ShouldBe(6); + noGcUnsafeCount.ShouldBe(6); + + Helpers.CollectAndWait(); + + c.Clean(); + c.AddCountSinceLastClean.ShouldBe(0); + c.UnsafeCount.ShouldBe(3); + + GC.KeepAlive(x); + } + + [TestMethod] + public void EnumerationCleaning() + { + var c = new WeakCollection(); + object x = new(); + + int noGcAddCountSinceLastClean; + int noGcUnsafeCount; + + using (NoGCRegion.Enter(10000)) + { + c.Add(x); + c.Add(x); + c.Add(x); + + AddCollectableItems(c, 3); + + noGcAddCountSinceLastClean = c.AddCountSinceLastClean; + noGcUnsafeCount = c.UnsafeCount; + } + + noGcAddCountSinceLastClean.ShouldBe(6); + noGcUnsafeCount.ShouldBe(6); + + Helpers.CollectAndWait(); + + foreach (object o in c) + { } + + c.Remove(x).ShouldBeTrue(); + c.Remove(x).ShouldBeTrue(); + +#if NET48 // NS2.0 target does not support removing stale entries as items are encountered. + c.AddCountSinceLastClean.ShouldBe(6); + c.UnsafeCount.ShouldBe(4); +#else + c.AddCountSinceLastClean.ShouldBe(0); + c.UnsafeCount.ShouldBe(1); +#endif + + GC.KeepAlive(x); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void AddCollectableItems(WeakCollection c, int count) + { + for (int i = 0; i < count; i++) + c.Add(new object()); + } +} diff --git a/Tests/Singulink.Collections.Weak.Tests/WeakListTests.cs b/Tests/Singulink.Collections.Weak.Tests/WeakListTests.cs new file mode 100644 index 0000000..521a623 --- /dev/null +++ b/Tests/Singulink.Collections.Weak.Tests/WeakListTests.cs @@ -0,0 +1,52 @@ +using System.Runtime.CompilerServices; + +namespace Singulink.Collections.Weak.Tests; + +[PrefixTestClass] +public class WeakListTests +{ + [TestMethod] + public void Clean() + { + var c = new WeakList(); + object x = new(); + + int noGcAddCountSinceLastClean; + int noGcUnsafeCount; + + using (NoGCRegion.Enter(10000)) + { + AddCollectableItems(c, 3); + + c.InsertFirst(x); + c.InsertAfter(x, x); + c.InsertBefore(x, x); + + noGcAddCountSinceLastClean = c.AddCountSinceLastClean; + noGcUnsafeCount = c.UnsafeCount; + } + + noGcAddCountSinceLastClean.ShouldBe(6); + noGcUnsafeCount.ShouldBe(6); + c.Take(3).ShouldBe([x, x, x]); + + Helpers.CollectAndWait(); + + c.Clean(); + c.AddCountSinceLastClean.ShouldBe(0); + c.UnsafeCount.ShouldBe(3); + + c.Remove(x); + c.Remove(x); + c.UnsafeCount.ShouldBe(1); + + GC.KeepAlive(x); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void AddCollectableItems(WeakList c, int count) + { + for (int i = 0; i < count; i++) + c.Add(new object()); + } +} diff --git a/Tests/Singulink.Collections.Weak.Tests/WeakValueDictionaryTests.cs b/Tests/Singulink.Collections.Weak.Tests/WeakValueDictionaryTests.cs new file mode 100644 index 0000000..25d37c5 --- /dev/null +++ b/Tests/Singulink.Collections.Weak.Tests/WeakValueDictionaryTests.cs @@ -0,0 +1,99 @@ +using System.Runtime.CompilerServices; + +namespace Singulink.Collections.Weak.Tests; + +[PrefixTestClass] +public class WeakValueDictionaryTests +{ + [TestMethod] + public void Clean() + { + var c = new WeakValueDictionary(); + object x = new(); + + int noGcAddCountSinceLastClean; + int noGcUnsafeCount; + + using (NoGCRegion.Enter(10000)) + { + c.Add(0, x); + c.Add(1, x); + c.Add(2, x); + + AddCollectableItems(c, 3, 3); + + noGcAddCountSinceLastClean = c.AddCountSinceLastClean; + noGcUnsafeCount = c.UnsafeCount; + } + + noGcAddCountSinceLastClean.ShouldBe(6); + noGcUnsafeCount.ShouldBe(6); + + Helpers.CollectAndWait(); + + c.Clean(); + c.AddCountSinceLastClean.ShouldBe(0); + c.UnsafeCount.ShouldBe(3); + + GC.KeepAlive(x); + } + + [TestMethod] + public void EnumerationCleaning() + { + var c = new WeakValueDictionary(); + object x = new(); + + int noGcAddCountSinceLastClean; + int noGcUnsafeCount; + bool noGcContainsKey4; + + using (NoGCRegion.Enter(10000)) + { + c.Add(0, x); + c.Add(1, x); + c.Add(2, x); + + AddCollectableItems(c, 3, 3); + + noGcAddCountSinceLastClean = c.AddCountSinceLastClean; + noGcUnsafeCount = c.UnsafeCount; + + noGcContainsKey4 = c.ContainsKey(4); + } + + noGcAddCountSinceLastClean.ShouldBe(6); + noGcUnsafeCount.ShouldBe(6); + + noGcContainsKey4.ShouldBeTrue(); + c.ContainsKey(1).ShouldBeTrue(); + + Helpers.CollectAndWait(); + + c.ContainsKey(4).ShouldBeFalse(); + c.ContainsKey(1).ShouldBeTrue(); + + foreach (object o in c) { } + + c.Remove(0).ShouldBeTrue(); + c.Remove(1).ShouldBeTrue(); + c.Remove(4).ShouldBeFalse(); + +#if NET48 // NS2.0 target does not support removing stale entries as items are encountered. + c.AddCountSinceLastClean.ShouldBe(6); + c.UnsafeCount.ShouldBe(3); +#else + c.AddCountSinceLastClean.ShouldBe(0); + c.UnsafeCount.ShouldBe(1); +#endif + + GC.KeepAlive(x); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void AddCollectableItems(WeakValueDictionary c, int startKey, int count) + { + for (int i = 0; i < count; i++) + c.Add(startKey++, new object()); + } +}