File: Memoization\MemoizedFunction.cs
Web Access
Project: src\src\Shared\Shared.csproj (Shared)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using Microsoft.Shared.Diagnostics;
 
#pragma warning disable CA1716
namespace Microsoft.Shared.Memoization;
#pragma warning restore CA1716
 
#pragma warning disable SA1402 // File may only contain a single type
 
/// <summary>
/// Memoizer for functions of arity 1.
/// </summary>
/// <remarks>
/// We don't use weak references because those can only wrap reference types, and we wish to support functions that return other kinds of values.
/// </remarks>
/// <typeparam name="TParameter">Input parameter type for the memoized function.</typeparam>
/// <typeparam name="TResult">Return type for the memoized function.</typeparam>
 
#if !SHARED_PROJECT
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
#endif
 
[DebuggerDisplay("{_values.Count} memoized values")]
internal sealed class MemoizedFunction<TParameter, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] TResult>
{
    private const int Concurrency = 10;
    private const int Capacity = 100;
 
    // Using a readonly struct means that we can assert in the ConcurrentDictionary
    // declaration that all keys are non-null. Otherwise we need to say so in the
    // type declaration ("where TParameter : notnull"), which forces users to care.
    internal readonly struct Arg : IEquatable<MemoizedFunction<TParameter, TResult>.Arg>
    {
        private readonly int _hash;
 
        public Arg(TParameter arg1)
        {
            Arg1 = arg1;
            _hash = Arg1?.GetHashCode() ?? 0;
        }
 
        public readonly TParameter Arg1;
 
        public override bool Equals(object? obj) => obj is MemoizedFunction<TParameter, TResult>.Arg arg && Equals(arg);
 
        public bool Equals(MemoizedFunction<TParameter, TResult>.Arg other) => EqualityComparer<TParameter>.Default.Equals(Arg1, other.Arg1);
 
        public override int GetHashCode() => _hash;
    }
 
    private readonly ConcurrentDictionary<Arg, Lazy<TResult>> _values;
 
    private readonly Func<TParameter, TResult> _function;
 
    /// <summary>
    /// Initializes a new instance of the <see cref="MemoizedFunction{TParameter1, TResult}"/> class.
    /// </summary>
    /// <param name="function">The function whose results will be memoized.</param>
    public MemoizedFunction(Func<TParameter, TResult> function)
    {
        _function = Throw.IfNull(function);
        _values = new(Concurrency, Capacity);
    }
 
    internal TResult Function(TParameter arg1)
    {
        var arg = new Arg(arg1);
 
        // Stryker disable once all
        if (_values.TryGetValue(arg, out var result))
        {
            return result.Value;
        }
 
        return _values.GetOrAdd(arg, new Lazy<TResult>(() => _function(arg1))).Value;
    }
}
 
/// <summary>
/// Memoizer for functions of arity 2.
/// </summary>
/// <typeparam name="TParameter1">First input parameter type for the memoized function.</typeparam>
/// <typeparam name="TParameter2">Second input parameter type for the memoized function.</typeparam>
/// <typeparam name="TResult">Return type for the memoized function.</typeparam>
 
#if !SHARED_PROJECT
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
#endif
 
[SuppressMessage(
        "Major Code Smell",
        "S2436:Types and methods should not have too many generic parameters",
        Justification = "We're using many generic types for the same reason Func<>, Func<,>, Func<,,>, ... exist.")]
[DebuggerDisplay("{_values.Count} memoized values")]
internal sealed class MemoizedFunction<TParameter1, TParameter2, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] TResult>
{
    private const int Concurrency = 10;
    private const int Capacity = 100;
 
    internal readonly struct Args : IEquatable<MemoizedFunction<TParameter1, TParameter2, TResult>.Args>
    {
        private readonly int _hash;
 
        public Args(TParameter1 arg1, TParameter2 arg2)
        {
            Arg1 = arg1;
            Arg2 = arg2;
            _hash = HashCode.Combine(Arg1, Arg2);
        }
 
        public readonly TParameter1 Arg1;
 
        public readonly TParameter2 Arg2;
 
        public override bool Equals(object? obj) => obj is MemoizedFunction<TParameter1, TParameter2, TResult>.Args args && Equals(args);
 
        public bool Equals(MemoizedFunction<TParameter1, TParameter2, TResult>.Args other) =>
               EqualityComparer<TParameter1>.Default.Equals(Arg1, other.Arg1)
            && EqualityComparer<TParameter2>.Default.Equals(Arg2, other.Arg2);
 
        public override int GetHashCode() => _hash;
    }
 
    private readonly ConcurrentDictionary<Args, Lazy<TResult>> _values;
 
    private readonly Func<TParameter1, TParameter2, TResult> _function;
 
    /// <summary>
    /// Initializes a new instance of the <see cref="MemoizedFunction{TParameter1, TParameter2, TResult}"/> class.
    /// </summary>
    /// <param name="function">The function whose results will be memoized.</param>
    public MemoizedFunction(Func<TParameter1, TParameter2, TResult> function)
    {
        _function = Throw.IfNull(function);
        _values = new(Concurrency, Capacity);
    }
 
    internal TResult Function(TParameter1 arg1, TParameter2 arg2)
    {
        var args = new Args(arg1, arg2);
 
        // Stryker disable once all
        if (_values.TryGetValue(args, out var result))
        {
            return result.Value;
        }
 
        return _values.GetOrAdd(args, new Lazy<TResult>(() => _function(arg1, arg2))).Value;
    }
}