diff --git a/LazyInterface/LazyInterface.Tests/.editorconfig b/LazyInterface/LazyInterface.Tests/.editorconfig new file mode 100644 index 0000000..c017690 --- /dev/null +++ b/LazyInterface/LazyInterface.Tests/.editorconfig @@ -0,0 +1,124 @@ +[*.cs] + +# SA1600: Elements should be documented +dotnet_diagnostic.SA1600.severity = none +csharp_indent_labels = one_less_than_current +csharp_style_throw_expression = true:suggestion +csharp_style_prefer_null_check_over_type_check = true:suggestion + +[*.{cs,vb}] +dotnet_style_operator_placement_when_wrapping = beginning_of_line +tab_width = 4 +indent_size = 4 +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_null_propagation = true:suggestion +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion +dotnet_style_prefer_auto_properties = true:silent +dotnet_style_object_initializer = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_prefer_simplified_boolean_expressions = true:suggestion +dotnet_style_prefer_conditional_expression_over_assignment = true:silent +dotnet_style_prefer_conditional_expression_over_return = true:silent +dotnet_style_explicit_tuple_names = true:suggestion +dotnet_style_prefer_inferred_tuple_names = true:suggestion +dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion +dotnet_style_prefer_compound_assignment = true:suggestion +dotnet_style_prefer_simplified_interpolation = true:suggestion +dotnet_style_prefer_collection_expression = when_types_loosely_match:suggestion +dotnet_style_namespace_match_folder = true:suggestion +[*.cs] +#### Стили именования #### + +# Правила именования + +dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion +dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface +dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i + +dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.types_should_be_pascal_case.symbols = types +dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case + +dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members +dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case + +# Спецификации символов + +dotnet_naming_symbols.interface.applicable_kinds = interface +dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.interface.required_modifiers = + +dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum +dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.types.required_modifiers = + +dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method +dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.non_field_members.required_modifiers = + +# Стили именования + +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 = +dotnet_naming_style.begins_with_i.capitalization = pascal_case + +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.capitalization = pascal_case + +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.capitalization = pascal_case +csharp_prefer_simple_default_expression = true:suggestion + +[*.vb] +#### Стили именования #### + +# Правила именования + +dotnet_naming_rule.interface_should_be_начинается_с_i.severity = suggestion +dotnet_naming_rule.interface_should_be_начинается_с_i.symbols = interface +dotnet_naming_rule.interface_should_be_начинается_с_i.style = начинается_с_i + +dotnet_naming_rule.типы_should_be_всечастиспрописнойбуквы.severity = suggestion +dotnet_naming_rule.типы_should_be_всечастиспрописнойбуквы.symbols = типы +dotnet_naming_rule.типы_should_be_всечастиспрописнойбуквы.style = всечастиспрописнойбуквы + +dotnet_naming_rule.не_являющиеся_полем_члены_should_be_всечастиспрописнойбуквы.severity = suggestion +dotnet_naming_rule.не_являющиеся_полем_члены_should_be_всечастиспрописнойбуквы.symbols = не_являющиеся_полем_члены +dotnet_naming_rule.не_являющиеся_полем_члены_should_be_всечастиспрописнойбуквы.style = всечастиспрописнойбуквы + +# Спецификации символов + +dotnet_naming_symbols.interface.applicable_kinds = interface +dotnet_naming_symbols.interface.applicable_accessibilities = public, friend, private, protected, protected_friend, private_protected +dotnet_naming_symbols.interface.required_modifiers = + +dotnet_naming_symbols.типы.applicable_kinds = class, struct, interface, enum +dotnet_naming_symbols.типы.applicable_accessibilities = public, friend, private, protected, protected_friend, private_protected +dotnet_naming_symbols.типы.required_modifiers = + +dotnet_naming_symbols.не_являющиеся_полем_члены.applicable_kinds = property, event, method +dotnet_naming_symbols.не_являющиеся_полем_члены.applicable_accessibilities = public, friend, private, protected, protected_friend, private_protected +dotnet_naming_symbols.не_являющиеся_полем_члены.required_modifiers = + +# Стили именования + +dotnet_naming_style.начинается_с_i.required_prefix = I +dotnet_naming_style.начинается_с_i.required_suffix = +dotnet_naming_style.начинается_с_i.word_separator = +dotnet_naming_style.начинается_с_i.capitalization = pascal_case + +dotnet_naming_style.всечастиспрописнойбуквы.required_prefix = +dotnet_naming_style.всечастиспрописнойбуквы.required_suffix = +dotnet_naming_style.всечастиспрописнойбуквы.word_separator = +dotnet_naming_style.всечастиспрописнойбуквы.capitalization = pascal_case + +dotnet_naming_style.всечастиспрописнойбуквы.required_prefix = +dotnet_naming_style.всечастиспрописнойбуквы.required_suffix = +dotnet_naming_style.всечастиспрописнойбуквы.word_separator = +dotnet_naming_style.всечастиспрописнойбуквы.capitalization = pascal_case diff --git a/LazyInterface/LazyInterface.Tests/CommonTests.cs b/LazyInterface/LazyInterface.Tests/CommonTests.cs new file mode 100644 index 0000000..d640c72 --- /dev/null +++ b/LazyInterface/LazyInterface.Tests/CommonTests.cs @@ -0,0 +1,73 @@ +// +// Copyright (c) Kalinin Andrew. All rights reserved. +// + +namespace LazyInterface.Tests; + +public class CommonTests +{ + public static IEnumerable> LazyImplementations() + { + yield return new SimpleVersion(() => new object()); + yield return new MultithreadedVersion(() => new object()); + } + + [TestCaseSource(nameof(LazyImplementations))] + public void Get_MultipleCalls_ShoulCallSupplierOnlyOnce(ILazy lazy) + { + int callCount = 0; + Func supplier = () => + { + callCount++; + return new object(); + }; + lazy = lazy is SimpleVersion ? new SimpleVersion(supplier) : new MultithreadedVersion(supplier); + + lazy.Get(); + lazy.Get(); + lazy.Get(); + + var result = lazy.Get(); + + Assert.That(callCount, Is.EqualTo(1)); + } + + [TestCaseSource(nameof(LazyImplementations))] + public void Get_MultipleCalls_ShoulReturnTheSameObjectReference(ILazy lazy) + { + lazy = lazy is SimpleVersion ? new SimpleVersion(() => new object()) : new MultithreadedVersion(() => new object()); + + var result1 = lazy.Get(); + var result2 = lazy.Get(); + Assert.That(result1, Is.SameAs(result2)); + } + + [TestCaseSource(nameof(LazyImplementations))] + public void Get_SupplierReturnsNull_ShouldReturnNull(ILazy lazy) + { + int callCount = 0; + lazy = lazy is SimpleVersion ? new SimpleVersion(() => + { + callCount++; + return null!; + }) : new MultithreadedVersion(() => + { + callCount++; + return null!; + }); + + var result1 = lazy.Get(); + var result2 = lazy.Get(); + + Assert.That(callCount, Is.EqualTo(1)); + Assert.That(result1, Is.Null); + Assert.That(result2, Is.Null); + } + + [TestCaseSource(nameof(LazyImplementations))] + public void Constructor_SupplierIsNull_ShouldThrowArgumentNullException(ILazy lazy) + { + Assert.Throws(() => new SimpleVersion(null!)); + Assert.Throws(() => new MultithreadedVersion(null!)); + } +} \ No newline at end of file diff --git a/LazyInterface/LazyInterface.Tests/LazyInterface.Tests.csproj b/LazyInterface/LazyInterface.Tests/LazyInterface.Tests.csproj new file mode 100644 index 0000000..a0feeff --- /dev/null +++ b/LazyInterface/LazyInterface.Tests/LazyInterface.Tests.csproj @@ -0,0 +1,39 @@ + + + + net9.0 + latest + enable + enable + false + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + + + + diff --git a/LazyInterface/LazyInterface.Tests/MultithreadedTests.cs b/LazyInterface/LazyInterface.Tests/MultithreadedTests.cs new file mode 100644 index 0000000..5e76e48 --- /dev/null +++ b/LazyInterface/LazyInterface.Tests/MultithreadedTests.cs @@ -0,0 +1,45 @@ +// +// Copyright (c) Kalinin Andrew. All rights reserved. +// + +namespace LazyInterface.Tests; + +public class MultithreadedTests +{ + [Test] + public void Get_AvailabilityOfRaces_SupplierShouldInitializeOnlyOnce() + { + int callCount = 0; + var lazy = new MultithreadedVersion(() => + { + Interlocked.Increment(ref callCount); + Thread.Sleep(150); + return new object(); + }); + + int numOfThreads = 10; + var threads = new Thread[numOfThreads]; + var results = new object?[numOfThreads]; + var barrier = new Barrier(numOfThreads); + + for (int i = 0; i < numOfThreads; ++i) + { + int index = i; + threads[i] = new Thread(() => + { + barrier.SignalAndWait(); + results[index] = lazy.Get(); + }); + threads[i].Start(); + } + + foreach (var thread in threads) + { + thread.Join(); + } + + Assert.That(callCount, Is.EqualTo(1)); + var firstResult = results[0]; + Assert.That(results.All(res => ReferenceEquals(res, firstResult)), Is.True); + } +} \ No newline at end of file diff --git a/LazyInterface/LazyInterface.Tests/stylecop.json b/LazyInterface/LazyInterface.Tests/stylecop.json new file mode 100644 index 0000000..ce12f73 --- /dev/null +++ b/LazyInterface/LazyInterface.Tests/stylecop.json @@ -0,0 +1,9 @@ +{ + "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json", + "settings": { + "documentationRules": { + "companyName": "Kalinin Andrew", + "copyrightText": "Copyright (c) {companyName}. All rights reserved." + } + } +} diff --git a/LazyInterface/LazyInterface.sln b/LazyInterface/LazyInterface.sln new file mode 100644 index 0000000..3a0e729 --- /dev/null +++ b/LazyInterface/LazyInterface.sln @@ -0,0 +1,31 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.13.35806.99 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LazyInterface", "LazyInterface\LazyInterface.csproj", "{492B0043-2F2B-4D4B-9032-8C3EC5A941C5}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LazyInterface.Tests", "LazyInterface.Tests\LazyInterface.Tests.csproj", "{38FDFD18-1B23-483D-A68C-9938CF478AD4}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {492B0043-2F2B-4D4B-9032-8C3EC5A941C5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {492B0043-2F2B-4D4B-9032-8C3EC5A941C5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {492B0043-2F2B-4D4B-9032-8C3EC5A941C5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {492B0043-2F2B-4D4B-9032-8C3EC5A941C5}.Release|Any CPU.Build.0 = Release|Any CPU + {38FDFD18-1B23-483D-A68C-9938CF478AD4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {38FDFD18-1B23-483D-A68C-9938CF478AD4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {38FDFD18-1B23-483D-A68C-9938CF478AD4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {38FDFD18-1B23-483D-A68C-9938CF478AD4}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {ADEC834D-6E46-477C-871F-21879B4BA23D} + EndGlobalSection +EndGlobal diff --git a/LazyInterface/LazyInterface/ILazy.cs b/LazyInterface/LazyInterface/ILazy.cs new file mode 100644 index 0000000..753fbff --- /dev/null +++ b/LazyInterface/LazyInterface/ILazy.cs @@ -0,0 +1,19 @@ +// +// Copyright (c) Kalinin Andrew. All rights reserved. +// + +namespace LazyInterface; + +/// +/// Interface for lazy value calculation. +/// +/// Type of calculated value. +public interface ILazy +{ + /// + /// Gets the calculated value. Performs the calculation on the first call, + /// returns the previously calculated value on subsequent attempts. + /// + /// Calculated value of type T. + T? Get(); +} \ No newline at end of file diff --git a/LazyInterface/LazyInterface/LazyInterface.csproj b/LazyInterface/LazyInterface/LazyInterface.csproj new file mode 100644 index 0000000..e25af34 --- /dev/null +++ b/LazyInterface/LazyInterface/LazyInterface.csproj @@ -0,0 +1,25 @@ + + + + Library + net9.0 + enable + enable + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + diff --git a/LazyInterface/LazyInterface/MultithreadedVersion.cs b/LazyInterface/LazyInterface/MultithreadedVersion.cs new file mode 100644 index 0000000..9189b13 --- /dev/null +++ b/LazyInterface/LazyInterface/MultithreadedVersion.cs @@ -0,0 +1,51 @@ +// +// Copyright (c) Kalinin Andrew. All rights reserved. +// + +namespace LazyInterface; + +/// +/// Thread-safe implementation of lazy evaluation. +/// +/// Type of calculated value. +public class MultithreadedVersion : ILazy +{ + private readonly object locker = new object(); + + private volatile Func? supplier; + + private T? result; + + /// + /// Initializes a new instance of the class. + /// + /// A function that calculates the value. + /// If supplier is null. + public MultithreadedVersion(Func supplier) + { + ArgumentNullException.ThrowIfNull(supplier); + + this.supplier = supplier; + } + + /// + /// Returns the calculated value. Guarantees a one-time calculation. + /// + /// Calculated value. + public T? Get() + { + if (this.supplier is not null) + { + lock (this.locker) + { + if (this.supplier is not null) + { + this.result = this.supplier(); + this.supplier = null; + } + } + } + + return this.result; + } +} \ No newline at end of file diff --git a/LazyInterface/LazyInterface/SimpleVersion.cs b/LazyInterface/LazyInterface/SimpleVersion.cs new file mode 100644 index 0000000..546fd2b --- /dev/null +++ b/LazyInterface/LazyInterface/SimpleVersion.cs @@ -0,0 +1,47 @@ +// +// Copyright (c) Kalinin Andrew. All rights reserved. +// + +namespace LazyInterface; + +/// +/// Single-threaded implementation of lazy evaluation. +/// +/// Type of calculated value. +public class SimpleVersion : ILazy +{ + private Func? supplier; + + private T? result; + + private bool isCalculated; + + /// + /// Initializes a new instance of the class. + /// + /// A function that calculates the value. + /// If supplier is null. + public SimpleVersion(Func supplier) + { + ArgumentNullException.ThrowIfNull(supplier); + + this.supplier = supplier; + } + + /// + /// Returns the calculated value. Performs the calculation on the first call, + /// returns the previously calculated value on subsequent attempts. + /// + /// Calculated value. + public T? Get() + { + if (!this.isCalculated) + { + this.result = this.supplier!(); + this.isCalculated = true; + this.supplier = null; + } + + return this.result; + } +} \ No newline at end of file diff --git a/LazyInterface/LazyInterface/stylecop.json b/LazyInterface/LazyInterface/stylecop.json new file mode 100644 index 0000000..ce12f73 --- /dev/null +++ b/LazyInterface/LazyInterface/stylecop.json @@ -0,0 +1,9 @@ +{ + "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json", + "settings": { + "documentationRules": { + "companyName": "Kalinin Andrew", + "copyrightText": "Copyright (c) {companyName}. All rights reserved." + } + } +}