Skip to content

Commit

Permalink
Add UnitsNetSetup to hold global static state
Browse files Browse the repository at this point in the history
Fix static init racing condition by replacing {get;} with =>
  • Loading branch information
angularsen committed Jun 18, 2023
1 parent 439c143 commit f540d8d
Show file tree
Hide file tree
Showing 7 changed files with 157 additions and 46 deletions.
10 changes: 1 addition & 9 deletions UnitsNet/CustomCode/Quantity.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,7 @@ namespace UnitsNet
{
public partial class Quantity
{
static Quantity()
{
Default = new QuantityInfoLookup();
}

private static QuantityInfoLookup Default
{
get;
}
private static QuantityInfoLookup Default => UnitsNetSetup.Default.QuantityInfoLookup;

/// <summary>
/// All enum value names of <see cref="Infos"/>, such as "Length" and "Mass".
Expand Down
8 changes: 2 additions & 6 deletions UnitsNet/CustomCode/QuantityParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ public class QuantityParser
/// <summary>
/// The default instance of <see cref="QuantityParser"/>, which uses <see cref="UnitAbbreviationsCache.Default"/> unit abbreviations.
/// </summary>
public static QuantityParser Default { get; }
[Obsolete("Use UnitsNetSetup.Default.QuantityParser instead.")]
public static QuantityParser Default => UnitsNetSetup.Default.QuantityParser;

/// <summary>
/// Creates an instance of <see cref="QuantityParser"/>, optionally specifying an <see cref="UnitAbbreviationsCache"/>
Expand All @@ -50,11 +51,6 @@ public QuantityParser(UnitAbbreviationsCache? unitAbbreviationsCache = null)
_unitParser = new UnitParser(_unitAbbreviationsCache);
}

static QuantityParser()
{
Default = new QuantityParser(UnitAbbreviationsCache.Default);
}

/// <summary>
/// Parses a quantity from a string, such as "1.2 kg" to <see cref="Length"/> or "100 cm" to <see cref="Mass"/>.
/// </summary>
Expand Down
33 changes: 29 additions & 4 deletions UnitsNet/CustomCode/UnitAbbreviationsCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,23 +30,48 @@ public sealed class UnitAbbreviationsCache
/// <summary>
/// The static instance used internally for ToString() and Parse() of quantities and units.
/// </summary>
public static UnitAbbreviationsCache Default { get; }
[Obsolete("Use UnitsNetSetup.Default.UnitAbbreviations instead.")]
public static UnitAbbreviationsCache Default => UnitsNetSetup.Default.UnitAbbreviations;

private QuantityInfoLookup QuantityInfoLookup { get; }

/// <summary>
/// Create an instance of the cache and load all the abbreviations defined in the library.
/// </summary>
// TODO Change this to create an empty cache in v6: https://github.com/angularsen/UnitsNet/issues/1200
[Obsolete("Use CreateDefault() instead to create an instance that loads the built-in units. The default ctor will change to create an empty cache in UnitsNet v6.")]
public UnitAbbreviationsCache()
: this(new QuantityInfoLookup(Quantity.ByName.Values))
{
QuantityInfoLookup= new QuantityInfoLookup();
}

static UnitAbbreviationsCache()
/// <summary>
/// Creates an instance of the cache and load all the abbreviations defined in the library.
/// </summary>
/// <remarks>
/// Access type is <c>internal</c> until this class is matured and ready for external use.
/// </remarks>
internal UnitAbbreviationsCache(QuantityInfoLookup quantityInfoLookup)
{
Default = new UnitAbbreviationsCache();
QuantityInfoLookup = quantityInfoLookup;
}

/// <summary>
/// Create an instance with empty cache.
/// </summary>
/// <remarks>
/// Workaround until v6 changes the default ctor to create an empty cache.<br/>
/// </remarks>
/// <returns>Instance with empty cache.</returns>
// TODO Remove in v6: https://github.com/angularsen/UnitsNet/issues/1200
public static UnitAbbreviationsCache CreateEmpty() => new(new QuantityInfoLookup(new List<QuantityInfo>()));

/// <summary>
/// Create an instance of the cache and load all the built-in unit abbreviations defined in the library.
/// </summary>
/// <returns>Instance with default abbreviations cache.</returns>
public static UnitAbbreviationsCache CreateDefault() => new(new QuantityInfoLookup(Quantity.ByName.Values));

/// <summary>
/// Adds one or more unit abbreviation for the given unit enum value.
/// This is used to dynamically add abbreviations for existing unit enums such as <see cref="UnitsNet.Units.LengthUnit"/> or to extend with third-party unit enums
Expand Down
9 changes: 3 additions & 6 deletions UnitsNet/CustomCode/UnitParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,22 +23,19 @@ public sealed class UnitParser
/// The default static instance used internally to parse quantities and units using the
/// default abbreviations cache for all units and abbreviations defined in the library.
/// </summary>
public static UnitParser Default { get; }
[Obsolete("Use UnitsNetSetup.Default.UnitParser instead.")]
public static UnitParser Default => UnitsNetSetup.Default.UnitParser;

/// <summary>
/// Create a parser using the given unit abbreviations cache.
/// </summary>
/// <param name="unitAbbreviationsCache"></param>
// TODO Change this to not fallback to built-in units abbreviations when given null, in v6: https://github.com/angularsen/UnitsNet/issues/1200
public UnitParser(UnitAbbreviationsCache? unitAbbreviationsCache)
{
_unitAbbreviationsCache = unitAbbreviationsCache ?? UnitAbbreviationsCache.Default;
}

static UnitParser()
{
Default = new UnitParser(UnitAbbreviationsCache.Default);
}

/// <summary>
/// Parses a unit abbreviation for a given unit enumeration type.
/// Example: Parse&lt;LengthUnit&gt;("km") => LengthUnit.Kilometer
Expand Down
85 changes: 85 additions & 0 deletions UnitsNet/CustomCode/UnitsNetSetup.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// Licensed under MIT No Attribution, see LICENSE file at the root.
// Copyright 2013 Andreas Gullberg Larsen ([email protected]). Maintained at https://github.com/angularsen/UnitsNet.

using System.Collections.Generic;
using UnitsNet.Units;

namespace UnitsNet;

/// <summary>
/// UnitsNet setup of quantities, units, unit abbreviations and conversion functions.<br />
/// <br />
/// For normal use, <see cref="Default" /> is used and can be manipulated to add new units or change default unit
/// abbreviations.<br />
/// Alternatively, a setup instance may be provided for most static methods, such as
/// <see cref="Quantity.Parse(System.Type,string)" /> and
/// <see cref="QuantityFormatter.Format{TUnitType}(UnitsNet.IQuantity{TUnitType},string)" />.
/// </summary>
public sealed class UnitsNetSetup
{
static UnitsNetSetup()
{
var unitConverter = UnitConverter.CreateDefault();
ICollection<QuantityInfo> quantityInfos = Quantity.ByName.Values;

Default = new UnitsNetSetup(quantityInfos, unitConverter);
}

/// <summary>
/// Create a new UnitsNet setup with the given quantities, their units and unit conversion functions between units.
/// </summary>
/// <param name="quantityInfos">The quantities and their units to support for unit conversions, Parse() and ToString().</param>
/// <param name="unitConverter">The unit converter instance.</param>
public UnitsNetSetup(ICollection<QuantityInfo> quantityInfos, UnitConverter unitConverter)
{
var quantityInfoLookup = new QuantityInfoLookup(quantityInfos);
var unitAbbreviations = new UnitAbbreviationsCache(quantityInfoLookup);

UnitConverter = unitConverter;
UnitAbbreviations = unitAbbreviations;
UnitParser = new UnitParser(unitAbbreviations);
QuantityParser = new QuantityParser(unitAbbreviations);
QuantityInfoLookup = quantityInfoLookup;
}

/// <summary>
/// The global default UnitsNet setup of quantities, units, unit abbreviations and conversion functions.
/// This setup is used by default in static Parse and ToString methods of quantities unless a setup instance is
/// provided.
/// </summary>
/// <remarks>
/// Manipulating this instance, such as adding new units or changing default unit abbreviations, will affect most
/// usages of UnitsNet in the
/// current AppDomain since the typical use is via static members and not providing a setup instance.
/// </remarks>
public static UnitsNetSetup Default { get; }

/// <summary>
/// Converts between units of a quantity, such as from meters to centimeters of a given length.
/// </summary>
public UnitConverter UnitConverter { get; }

/// <summary>
/// Maps unit enums to unit abbreviation strings for one or more cultures, used by ToString() and Parse() methods of
/// quantities.
/// </summary>
public UnitAbbreviationsCache UnitAbbreviations { get; }

/// <summary>
/// Parses units from strings, such as <see cref="LengthUnit.Centimeter" /> from "cm".
/// </summary>
public UnitParser UnitParser { get; }

/// <summary>
/// Parses quantities from strings, such as parsing <see cref="Mass" /> from "1.2 kg".
/// </summary>
internal QuantityParser QuantityParser { get; }

/// <summary>
/// The quantities and units that are loaded.
/// </summary>
/// <remarks>
/// Access type is <c>internal</c> until this class is matured and ready for external use.
/// </remarks>
internal QuantityInfoLookup QuantityInfoLookup { get; }
}
37 changes: 23 additions & 14 deletions UnitsNet/QuantityInfoLookup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,24 +9,27 @@ namespace UnitsNet
/// <summary>
/// A collection of <see cref="QuantityInfo"/>.
/// </summary>
public class QuantityInfoLookup
/// <remarks>
/// Access type is <c>internal</c> until this class is matured and ready for external use.
/// </remarks>
internal class QuantityInfoLookup
{
private readonly Lazy<QuantityInfo[]> InfosLazy;
private readonly Lazy<Dictionary<(Type, string), UnitInfo>> UnitTypeAndNameToUnitInfoLazy;
private readonly Lazy<QuantityInfo[]> _infosLazy;
private readonly Lazy<Dictionary<(Type, string), UnitInfo>> _unitTypeAndNameToUnitInfoLazy;

/// <summary>
/// New instance.
/// </summary>
public QuantityInfoLookup()
/// <param name="quantityInfos"></param>
public QuantityInfoLookup(ICollection<QuantityInfo> quantityInfos)
{
ICollection<QuantityInfo> quantityInfos = Quantity.ByName.Values;
Names = quantityInfos.Select(qt => qt.Name).ToArray();

InfosLazy = new Lazy<QuantityInfo[]>(() => quantityInfos
_infosLazy = new Lazy<QuantityInfo[]>(() => quantityInfos
.OrderBy(quantityInfo => quantityInfo.Name)
.ToArray());

UnitTypeAndNameToUnitInfoLazy = new Lazy<Dictionary<(Type, string), UnitInfo>>(() =>
_unitTypeAndNameToUnitInfoLazy = new Lazy<Dictionary<(Type, string), UnitInfo>>(() =>
{
return Infos
.SelectMany(quantityInfo => quantityInfo.UnitInfos
Expand All @@ -45,18 +48,18 @@ public QuantityInfoLookup()
/// <summary>
/// All quantity information objects, such as <see cref="Length.Info"/> and <see cref="Mass.Info"/>.
/// </summary>
public QuantityInfo[] Infos => InfosLazy.Value;
public QuantityInfo[] Infos => _infosLazy.Value;

/// <summary>
/// Get <see cref="UnitInfo"/> for a given unit enum value.
/// </summary>
public UnitInfo GetUnitInfo(Enum unitEnum) => UnitTypeAndNameToUnitInfoLazy.Value[(unitEnum.GetType(), unitEnum.ToString())];
public UnitInfo GetUnitInfo(Enum unitEnum) => _unitTypeAndNameToUnitInfoLazy.Value[(unitEnum.GetType(), unitEnum.ToString())];

/// <summary>
/// Try to get <see cref="UnitInfo"/> for a given unit enum value.
/// </summary>
public bool TryGetUnitInfo(Enum unitEnum, [NotNullWhen(true)] out UnitInfo? unitInfo) =>
UnitTypeAndNameToUnitInfoLazy.Value.TryGetValue((unitEnum.GetType(), unitEnum.ToString()), out unitInfo);
_unitTypeAndNameToUnitInfoLazy.Value.TryGetValue((unitEnum.GetType(), unitEnum.ToString()), out unitInfo);

/// <summary>
///
Expand All @@ -65,7 +68,7 @@ public bool TryGetUnitInfo(Enum unitEnum, [NotNullWhen(true)] out UnitInfo? unit
/// <param name="unitInfo"></param>
public void AddUnitInfo(Enum unit, UnitInfo unitInfo)
{
UnitTypeAndNameToUnitInfoLazy.Value.Add((unit.GetType(), unit.ToString()), unitInfo);
_unitTypeAndNameToUnitInfoLazy.Value.Add((unit.GetType(), unit.ToString()), unitInfo);
}

/// <summary>
Expand All @@ -77,6 +80,7 @@ public void AddUnitInfo(Enum unit, UnitInfo unitInfo)
/// <exception cref="ArgumentException">Unit value is not a know unit enum type.</exception>
public IQuantity From(QuantityValue value, Enum unit)
{
// TODO Support custom units, currently only hardcoded built-in quantities are supported.
return Quantity.TryFrom(value, unit, out IQuantity? quantity)
? quantity
: throw new UnitNotFoundException($"Unit value {unit} of type {unit.GetType()} is not a known unit enum type. Expected types like UnitsNet.Units.LengthUnit. Did you pass in a custom enum type defined outside the UnitsNet library?");
Expand All @@ -93,6 +97,7 @@ public bool TryFrom(double value, Enum unit, [NotNullWhen(true)] out IQuantity?
return false;
}

// TODO Support custom units, currently only hardcoded built-in quantities are supported.
return Quantity.TryFrom((QuantityValue)value, unit, out quantity);
}

Expand All @@ -112,23 +117,27 @@ public IQuantity Parse(IFormatProvider? formatProvider, Type quantityType, strin
if (!typeof(IQuantity).IsAssignableFrom(quantityType))
throw new ArgumentException($"Type {quantityType} must be of type UnitsNet.IQuantity.");

// TODO Support custom units, currently only hardcoded built-in quantities are supported.
if (Quantity.TryParse(formatProvider, quantityType, quantityString, out IQuantity? quantity))
return quantity;

throw new UnitNotFoundException($"Quantity string '{quantityString}' could not be parsed to quantity '{quantityType}'.");
}

/// <inheritdoc cref="Quantity.TryParse(IFormatProvider,System.Type,string,out UnitsNet.IQuantity)"/>
public bool TryParse(Type quantityType, string quantityString, [NotNullWhen(true)] out IQuantity? quantity) =>
Quantity.TryParse(null, quantityType, quantityString, out quantity);
public bool TryParse(Type quantityType, string quantityString, [NotNullWhen(true)] out IQuantity? quantity)
{
// TODO Support custom units, currently only hardcoded built-in quantities are supported.
return Quantity.TryParse(null, quantityType, quantityString, out quantity);
}

/// <summary>
/// Get a list of quantities that has the given base dimensions.
/// </summary>
/// <param name="baseDimensions">The base dimensions to match.</param>
public IEnumerable<QuantityInfo> GetQuantitiesWithBaseDimensions(BaseDimensions baseDimensions)
{
return InfosLazy.Value.Where(info => info.BaseDimensions.Equals(baseDimensions));
return _infosLazy.Value.Where(info => info.BaseDimensions.Equals(baseDimensions));
}
}
}
21 changes: 14 additions & 7 deletions UnitsNet/UnitConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,8 @@ public sealed class UnitConverter
/// The static instance used by Units.NET to convert between units. Modify this to add/remove conversion functions at runtime, such
/// as adding your own third-party units and quantities to convert between.
/// </summary>
public static UnitConverter Default { get; }

static UnitConverter()
{
Default = new UnitConverter();
RegisterDefaultConversions(Default);
}
[Obsolete("Use UnitsNetSetup.Default.UnitConverter instead.")]
public static UnitConverter Default => UnitsNetSetup.Default.UnitConverter;

/// <summary>
/// Creates a new <see cref="UnitConverter"/> instance.
Expand All @@ -64,6 +59,18 @@ public UnitConverter(UnitConverter other)
ConversionFunctions = new ConcurrentDictionary<ConversionFunctionLookupKey, ConversionFunction>(other.ConversionFunctions);
}

/// <summary>
/// Create an instance of the unit converter with all the built-in unit conversions defined in the library.
/// </summary>
/// <returns>The unit converter.</returns>
public static UnitConverter CreateDefault()
{
var unitConverter = new UnitConverter();
RegisterDefaultConversions(unitConverter);

return unitConverter;
}

private ConcurrentDictionary<ConversionFunctionLookupKey, ConversionFunction> ConversionFunctions
{
get;
Expand Down

0 comments on commit f540d8d

Please sign in to comment.