[C#] カルチャ依存問題からの完全脱却、インバリアントモードとコード分析の有効化
Windows 上の .NET 5 以降での破壊的変更です。文字列のソート、順序比較のデフォルトの動作が変更されました。パフォーマンスにも影響します。
実行環境に依存せず、一番良いパフォーマンスにする設定方法について記載します。
まずはシンプルなコードで変更内容を見てみましょう。
string[] src = [ "あ", "ア", "ア" ];
// 何らかのソート処理
src.OrderBy(x => x).ToArray();
Array.Sort(src);
// 結果
// ア ア あ (.NET 5 より前のデフォルト動作、.NET Framework も同じ)
// あ ア ア (.NET 5 以降のデフォルト動作)
これ以外の文字列でも、色々なメソッドで影響を受けます。
今回特に問題になるのは IComparer<string>
の引数があるメソッドです。
- ⚠️
IComparer<string>
の引数があるメソッドは要注意です。Comparer.Default は ICU 比較されるためです。 - ✅
IEqualityComparer<string>
の引数があるメソッドは問題ありません。EqualityComparer.Default は Ordinal 比較されるためです。
少なからずカルチャ依存問題は .NET Framework の頃からありました。
しかし、.NET Core 2.0 で導入されているインバリアントモードを有効にすることで、カルチャ依存問題から脱却できます。
環境
- .NET 8.0.8 (SDK 8.0.400)
- C# 12.0
- Visual Studio 2022 Version 17.11.2
- Windows 11 Pro 23H2 22631.4037
- Meziantou.Analyzer v2.0.163
- Microsoft.CodeAnalysis.BannedApiAnalyzers v3.3.4
前提
この対応を行う目的は、実行環境(地域・言語設定)により動作が異ならないようにすることと、性能を向上させることです。
CultureInfo.InvariantCulture
を使用して、カルチャに依存しないようになります。StringComparer.Ordinal
を使用して、バイナリ比較かつ高速化になります。- ❗ 任意の
CultureInfo (例えば ja-JP など)
が使用できなくなる点は注意です。
まず、アプリ開発をするのか、ライブラリ開発をするのかで、必要な対応方法が異なります。
最初にオススメの設定方法を示します。各設定の説明については後述します。
アプリ開発向けのオススメ設定方法
アプリ開発の場合は、インバリアントモードを有効にするだけで設定完了です。CultureInfo
を指定しても強制的に InvariantCulture
になるため、コーディング上で特に気を付けることはないと思います。
グローバリゼーション インバリアント モードの設定
以下の設定で、インバリアントモードを有効にします。
- InvariantGlobalization .NET 5 で導入 (詳しくは後述します)
- PredefinedCulturesOnly .NET 6 で導入 (詳しくは後述します)
アプロプロジェクトファイルの .csproj
に以下を追加します。
<PropertyGroup>
<InvariantGlobalization>true</InvariantGlobalization>
<PredefinedCulturesOnly>false</PredefinedCulturesOnly>
</PropertyGroup>
もし、アプリ以外にライブラリプロジェクトがある場合は、Directory.Build.props に設定してソリューション内の全てのプロジェクトに適用すると良いでしょう。
ライブラリプロジェクトでの余計なコード分析の通知を抑制することができます。
インバリアント カルチャの文字列フォーマットの設定
InvariantCulture
でも北米フォーマットになる部分があるため、ISO 8601 形式に変更します。必須ではありませんがオススメです。
- .NET のカルチャー依存 API 問題 | ++C++; // 未確認飛行 C ブログ
- https://github.com/ufcpp-live/UfcppLiveAgenda/issues/73#issuecomment-1595764176
アプリプロジェクトに以下のコードを追加します。ModuleInitializer を使用せず、エントリポイントの最初の方に実行しても良いと思います。
internal static class Initialier
{
[ModuleInitializer]
public static void Init()
{
var c = (CultureInfo)CultureInfo.InvariantCulture.Clone();
c.DateTimeFormat.LongDatePattern = "yyyy'-'MM'-'dd";
c.DateTimeFormat.LongTimePattern = "HH':'mm':'ss";
c.DateTimeFormat.MonthDayPattern = "MM'-'dd";
c.DateTimeFormat.YearMonthPattern = "yyyy'-'MM";
c.DateTimeFormat.ShortDatePattern = "yyyy'-'MM'-'dd";
c.DateTimeFormat.ShortTimePattern = "HH':'mm";
CultureInfo.DefaultThreadCurrentCulture = c;
}
}
詳しくは後述します。
アプリ開発向けの設定は、以上で完了です。
ライブラリ開発向けのオススメ設定方法
ライブラリ開発をする場合は、使用者側がインバリアントモードで実行するかどうかは分かりません。
そのため、明示的にインバリアントを指定するように自分でコーディングする必要があります。
コード分析を有効にして、カルチャ依存のコードを検出することが重要になります。
コード分析のエラー修正方法
下記で行う各コード分析を有効にすると、いくつかビルド警告(エラー)が発生するようになります。まずはエラーの修正方法について説明します。
以下のような方向で修正しましょう。
string.ToLower()
はstring.ToLowerInvariant()
に変更する。IFormatProvider
を指定するオーバーロードがある場合は、CultureInfo.InvariantCulture
を指定する。StringComparison
を指定するオーバーロードがある場合は、StringComparison.Ordinal
orOrdinalIgnoreCase
を指定する。IComparer<string>
を指定するオーバーロードがある場合は、StringComparer.Ordinal
orOrdinalIgnoreCase
を指定する。- 文字列補間($)は、String.Create(IFormatProvider, DefaultInterpolatedStringHandler) を使用する。
- ❗ 外部ライブラリの処理で、内部的に
ToString()
される可能性があるメソッドは使用しない、もしくは事前にstring
にして指定する。
修正イメージです。
string str = "abc";
- str.ToLower();
+ str.ToLowerInvariant();
- str.IndexOf("bc");
+ str.IndexOf("bc", StringComparison.Ordinal);
- float.TryParse(str, out _);
+ float.TryParse(str, CultureInfo.InvariantCulture, out _);
float num = 1.23;
- num.ToString();
+ num.ToString(CultureInfo.InvariantCulture);
string[] strings = new string[] { "A", "a" };
- strings.OrderBy(x => x).ToArray();
+ strings.OrderBy(x => x, StringComparer.Ordinal).ToArray();
.NET コード分析の設定
EnableNETAnalyzers は .NET 5 以降で既定で有効になりますが、netstandard などで有効にするには明示的に設定する必要があります。
Directory.Build.props
または各ライブラリ .csproj
に以下を追加します。
<Project>
<PropertyGroup>
<EnableNETAnalyzers>true</EnableNETAnalyzers>
<WarningsAsErrors>$(WarningsAsErrors);CA1304;CA1305;CA1309;CA1310;CA1311</WarningsAsErrors>
</PropertyGroup>
</Project>
<WarningsAsErrors>
の設定については、.editorconfig に記述した方が見やすく、柔軟に設定できます。ただし、<WarningsAsErrors>
の設定が優先されます。
[*.{cs,vb}]
dotnet_diagnostic.CA1304.severity = error
dotnet_diagnostic.CA1305.severity = error
dotnet_diagnostic.CA1309.severity = error
dotnet_diagnostic.CA1310.severity = error
dotnet_diagnostic.CA1311.severity = error
ルールについて詳しくは後述します。
Meziantou.Analyzer コード分析の設定
前述の .NET コード分析 だけでは、コード検出が不十分な場合があります。
Meziantou.Analyzer が細かくコード分析してくれるため、NuGet から追加で導入します。
多数のコード分析が入っています。ここでは今回必要な最小限の設定のみを行うため、必要なルールのみ error
、その他は none
に設定してあります。error
に適用したルールについて詳しくは後述します。
それでは Global AnalyzerConfig の設定ファイル Meziantou.Analyzer.globalconfig
を作成し、以下の設定をします。
ℹ️ Meziantou.Analyzer v2.0.163
前提での設定です。
# https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/configuration-files#global-analyzerconfig
is_global = true
# https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/README.md
# MA0001: StringComparison is missing
dotnet_diagnostic.MA0001.severity = none
# MA0002: IEqualityComparer<string> or IComparer<string> is missing
dotnet_diagnostic.MA0002.severity = error
# MA0003: Add parameter name to improve readability
dotnet_diagnostic.MA0003.severity = none
# MA0004: Use Task.ConfigureAwait
dotnet_diagnostic.MA0004.severity = none
# MA0005: Use Array.Empty<T>()
dotnet_diagnostic.MA0005.severity = none
# MA0006: Use String.Equals instead of equality operator
dotnet_diagnostic.MA0006.severity = none
# MA0007: Add a comma after the last value
dotnet_diagnostic.MA0007.severity = none
# MA0008: Add StructLayoutAttribute
dotnet_diagnostic.MA0008.severity = none
# MA0009: Add regex evaluation timeout
dotnet_diagnostic.MA0009.severity = none
# MA0010: Mark attributes with AttributeUsageAttribute
dotnet_diagnostic.MA0010.severity = none
# MA0011: IFormatProvider is missing
dotnet_diagnostic.MA0011.severity = none
# MA0012: Do not raise reserved exception type
dotnet_diagnostic.MA0012.severity = none
# MA0013: Types should not extend System.ApplicationException
dotnet_diagnostic.MA0013.severity = none
# MA0014: Do not raise System.ApplicationException type
dotnet_diagnostic.MA0014.severity = none
# MA0015: Specify the parameter name in ArgumentException
dotnet_diagnostic.MA0015.severity = none
# MA0016: Prefer using collection abstraction instead of implementation
dotnet_diagnostic.MA0016.severity = none
# MA0017: Abstract types should not have public or internal constructors
dotnet_diagnostic.MA0017.severity = none
# MA0018: Do not declare static members on generic types (deprecated; use CA1000 instead)
dotnet_diagnostic.MA0018.severity = none
# MA0019: Use EventArgs.Empty
dotnet_diagnostic.MA0019.severity = none
# MA0020: Use direct methods instead of LINQ methods
dotnet_diagnostic.MA0020.severity = none
# MA0021: Use StringComparer.GetHashCode instead of string.GetHashCode
dotnet_diagnostic.MA0021.severity = none
# MA0022: Return Task.FromResult instead of returning null
dotnet_diagnostic.MA0022.severity = none
# MA0023: Add RegexOptions.ExplicitCapture
dotnet_diagnostic.MA0023.severity = none
# MA0024: Use an explicit StringComparer when possible
dotnet_diagnostic.MA0024.severity = none
# MA0025: Implement the functionality instead of throwing NotImplementedException
dotnet_diagnostic.MA0025.severity = none
# MA0026: Fix TODO comment
dotnet_diagnostic.MA0026.severity = none
# MA0027: Prefer rethrowing an exception implicitly
dotnet_diagnostic.MA0027.severity = none
# MA0028: Optimize StringBuilder usage
dotnet_diagnostic.MA0028.severity = none
# MA0029: Combine LINQ methods
dotnet_diagnostic.MA0029.severity = none
# MA0030: Remove useless OrderBy call
dotnet_diagnostic.MA0030.severity = none
# MA0031: Optimize Enumerable.Count() usage
dotnet_diagnostic.MA0031.severity = none
# MA0032: Use an overload with a CancellationToken argument
dotnet_diagnostic.MA0032.severity = none
# MA0033: Do not tag instance fields with ThreadStaticAttribute
dotnet_diagnostic.MA0033.severity = none
# MA0035: Do not use dangerous threading methods
dotnet_diagnostic.MA0035.severity = none
# MA0036: Make class static
dotnet_diagnostic.MA0036.severity = none
# MA0037: Remove empty statement
dotnet_diagnostic.MA0037.severity = none
# MA0038: Make method static (deprecated, use CA1822 instead)
dotnet_diagnostic.MA0038.severity = none
# MA0039: Do not write your own certificate validation method
dotnet_diagnostic.MA0039.severity = none
# MA0040: Forward the CancellationToken parameter to methods that take one
dotnet_diagnostic.MA0040.severity = none
# MA0041: Make property static (deprecated, use CA1822 instead)
dotnet_diagnostic.MA0041.severity = none
# MA0042: Do not use blocking calls in an async method
dotnet_diagnostic.MA0042.severity = none
# MA0043: Use nameof operator in ArgumentException
dotnet_diagnostic.MA0043.severity = none
# MA0044: Remove useless ToString call
dotnet_diagnostic.MA0044.severity = none
# MA0045: Do not use blocking calls in a sync method (need to make calling method async)
dotnet_diagnostic.MA0045.severity = none
# MA0046: Use EventHandler<T> to declare events
dotnet_diagnostic.MA0046.severity = none
# MA0047: Declare types in namespaces
dotnet_diagnostic.MA0047.severity = none
# MA0048: File name must match type name
dotnet_diagnostic.MA0048.severity = none
# MA0049: Type name should not match containing namespace
dotnet_diagnostic.MA0049.severity = none
# MA0050: Validate arguments correctly in iterator methods
dotnet_diagnostic.MA0050.severity = none
# MA0051: Method is too long
dotnet_diagnostic.MA0051.severity = none
# MA0052: Replace constant Enum.ToString with nameof
dotnet_diagnostic.MA0052.severity = none
# MA0053: Make class sealed
dotnet_diagnostic.MA0053.severity = none
# MA0054: Embed the caught exception as innerException
dotnet_diagnostic.MA0054.severity = none
# MA0055: Do not use finalizer
dotnet_diagnostic.MA0055.severity = none
# MA0056: Do not call overridable members in constructor
dotnet_diagnostic.MA0056.severity = none
# MA0057: Class name should end with 'Attribute'
dotnet_diagnostic.MA0057.severity = none
# MA0058: Class name should end with 'Exception'
dotnet_diagnostic.MA0058.severity = none
# MA0059: Class name should end with 'EventArgs'
dotnet_diagnostic.MA0059.severity = none
# MA0060: The value returned by Stream.Read/Stream.ReadAsync is not used
dotnet_diagnostic.MA0060.severity = none
# MA0061: Method overrides should not change default values
dotnet_diagnostic.MA0061.severity = none
# MA0062: Non-flags enums should not be marked with "FlagsAttribute"
dotnet_diagnostic.MA0062.severity = none
# MA0063: Use Where before OrderBy
dotnet_diagnostic.MA0063.severity = none
# MA0064: Avoid locking on publicly accessible instance
dotnet_diagnostic.MA0064.severity = none
# MA0065: Default ValueType.Equals or HashCode is used for struct equality
dotnet_diagnostic.MA0065.severity = none
# MA0066: Hash table unfriendly type is used in a hash table
dotnet_diagnostic.MA0066.severity = none
# MA0067: Use Guid.Empty
dotnet_diagnostic.MA0067.severity = none
# MA0068: Invalid parameter name for nullable attribute
dotnet_diagnostic.MA0068.severity = none
# MA0069: Non-constant static fields should not be visible
dotnet_diagnostic.MA0069.severity = none
# MA0070: Obsolete attributes should include explanations
dotnet_diagnostic.MA0070.severity = none
# MA0071: Avoid using redundant else
dotnet_diagnostic.MA0071.severity = none
# MA0072: Do not throw from a finally block
dotnet_diagnostic.MA0072.severity = none
# MA0073: Avoid comparison with bool constant
dotnet_diagnostic.MA0073.severity = none
# MA0074: Avoid implicit culture-sensitive methods
dotnet_diagnostic.MA0074.severity = none
# MA0075: Do not use implicit culture-sensitive ToString
dotnet_diagnostic.MA0075.severity = none
# MA0076: Do not use implicit culture-sensitive ToString in interpolated strings
dotnet_diagnostic.MA0076.severity = error
# MA0077: A class that provides Equals(T) should implement IEquatable<T>
dotnet_diagnostic.MA0077.severity = none
# MA0078: Use 'Cast' instead of 'Select' to cast
dotnet_diagnostic.MA0078.severity = none
# MA0079: Forward the CancellationToken using .WithCancellation()
dotnet_diagnostic.MA0079.severity = none
# MA0080: Use a cancellation token using .WithCancellation()
dotnet_diagnostic.MA0080.severity = none
# MA0081: Method overrides should not omit params keyword
dotnet_diagnostic.MA0081.severity = none
# MA0082: NaN should not be used in comparisons
dotnet_diagnostic.MA0082.severity = none
# MA0083: ConstructorArgument parameters should exist in constructors
dotnet_diagnostic.MA0083.severity = none
# MA0084: Local variables should not hide other symbols
dotnet_diagnostic.MA0084.severity = none
# MA0085: Anonymous delegates should not be used to unsubscribe from Events
dotnet_diagnostic.MA0085.severity = none
# MA0086: Do not throw from a finalizer
dotnet_diagnostic.MA0086.severity = none
# MA0087: Parameters with [DefaultParameterValue] attributes should also be marked [Optional]
dotnet_diagnostic.MA0087.severity = none
# MA0088: Use [DefaultParameterValue] instead of [DefaultValue]
dotnet_diagnostic.MA0088.severity = none
# MA0089: Optimize string method usage
dotnet_diagnostic.MA0089.severity = none
# MA0090: Remove empty else/finally block
dotnet_diagnostic.MA0090.severity = none
# MA0091: Sender should be 'this' for instance events
dotnet_diagnostic.MA0091.severity = none
# MA0092: Sender should be 'null' for static events
dotnet_diagnostic.MA0092.severity = none
# MA0093: EventArgs should not be null
dotnet_diagnostic.MA0093.severity = none
# MA0094: A class that provides CompareTo(T) should implement IComparable<T>
dotnet_diagnostic.MA0094.severity = none
# MA0095: A class that implements IEquatable<T> should override Equals(object)
dotnet_diagnostic.MA0095.severity = none
# MA0096: A class that implements IComparable<T> should also implement IEquatable<T>
dotnet_diagnostic.MA0096.severity = none
# MA0097: A class that implements IComparable<T> or IComparable should override comparison operators
dotnet_diagnostic.MA0097.severity = none
# MA0098: Use indexer instead of LINQ methods
dotnet_diagnostic.MA0098.severity = none
# MA0099: Use Explicit enum value instead of 0
dotnet_diagnostic.MA0099.severity = none
# MA0100: Await task before disposing of resources
dotnet_diagnostic.MA0100.severity = none
# MA0101: String contains an implicit end of line character
dotnet_diagnostic.MA0101.severity = none
# MA0102: Make member readonly
dotnet_diagnostic.MA0102.severity = none
# MA0103: Use SequenceEqual instead of equality operator
dotnet_diagnostic.MA0103.severity = none
# MA0104: Do not create a type with a name from the BCL
dotnet_diagnostic.MA0104.severity = none
# MA0105: Use the lambda parameters instead of using a closure
dotnet_diagnostic.MA0105.severity = none
# MA0106: Avoid closure by using an overload with the 'factoryArgument' parameter
dotnet_diagnostic.MA0106.severity = none
# MA0107: Do not use culture-sensitive object.ToString
dotnet_diagnostic.MA0107.severity = none
# MA0108: Remove redundant argument value
dotnet_diagnostic.MA0108.severity = none
# MA0109: Consider adding an overload with a Span<T> or Memory<T>
dotnet_diagnostic.MA0109.severity = none
# MA0110: Use the Regex source generator
dotnet_diagnostic.MA0110.severity = none
# MA0111: Use string.Create instead of FormattableString
dotnet_diagnostic.MA0111.severity = error
# MA0112: Use 'Count > 0' instead of 'Any()'
dotnet_diagnostic.MA0112.severity = none
# MA0113: Use DateTime.UnixEpoch
dotnet_diagnostic.MA0113.severity = none
# MA0114: Use DateTimeOffset.UnixEpoch
dotnet_diagnostic.MA0114.severity = none
# MA0115: Unknown component parameter
dotnet_diagnostic.MA0115.severity = none
# MA0116: Parameters with [SupplyParameterFromQuery] attributes should also be marked as [Parameter]
dotnet_diagnostic.MA0116.severity = none
# MA0117: Parameters with [EditorRequired] attributes should also be marked as [Parameter]
dotnet_diagnostic.MA0117.severity = none
# MA0118: [JSInvokable] methods must be public
dotnet_diagnostic.MA0118.severity = none
# MA0119: JSRuntime must not be used in OnInitialized or OnInitializedAsync
dotnet_diagnostic.MA0119.severity = none
# MA0120: Use InvokeVoidAsync when the returned value is not used
dotnet_diagnostic.MA0120.severity = none
# MA0121: Do not overwrite parameter value
dotnet_diagnostic.MA0121.severity = none
# MA0122: Parameters with [SupplyParameterFromQuery] attributes are only valid in routable components (@page)
dotnet_diagnostic.MA0122.severity = none
# MA0123: Sequence number must be a constant
dotnet_diagnostic.MA0123.severity = none
# MA0124: Log parameter type is not valid
dotnet_diagnostic.MA0124.severity = none
# MA0125: The list of log parameter types contains an invalid type
dotnet_diagnostic.MA0125.severity = none
# MA0126: The list of log parameter types contains a duplicate
dotnet_diagnostic.MA0126.severity = none
# MA0127: Use String.Equals instead of is pattern
dotnet_diagnostic.MA0127.severity = none
# MA0128: Use 'is' operator instead of SequenceEqual
dotnet_diagnostic.MA0128.severity = none
# MA0129: Await task in using statement
dotnet_diagnostic.MA0129.severity = none
# MA0130: GetType() should not be used on System.Type instances
dotnet_diagnostic.MA0130.severity = none
# MA0131: ArgumentNullException.ThrowIfNull should not be used with non-nullable types
dotnet_diagnostic.MA0131.severity = none
# MA0132: Do not convert implicitly to DateTimeOffset
dotnet_diagnostic.MA0132.severity = none
# MA0133: Use DateTimeOffset instead of relying on the implicit conversion
dotnet_diagnostic.MA0133.severity = none
# MA0134: Observe result of async calls
dotnet_diagnostic.MA0134.severity = none
# MA0135: The log parameter has no configured type
dotnet_diagnostic.MA0135.severity = none
# MA0136: Raw String contains an implicit end of line character
dotnet_diagnostic.MA0136.severity = none
# MA0137: Use 'Async' suffix when a method returns an awaitable type
dotnet_diagnostic.MA0137.severity = none
# MA0138: Do not use 'Async' suffix when a method does not return an awaitable type
dotnet_diagnostic.MA0138.severity = none
# MA0139: Log parameter type is not valid
dotnet_diagnostic.MA0139.severity = none
# MA0140: Both if and else branch have identical code
dotnet_diagnostic.MA0140.severity = none
# MA0141: Use pattern matching instead of inequality operators for null check
dotnet_diagnostic.MA0141.severity = none
# MA0142: Use pattern matching instead of equality operators for null check
dotnet_diagnostic.MA0142.severity = none
# MA0143: Primary constructor parameters should be readonly
dotnet_diagnostic.MA0143.severity = none
# MA0144: Use System.OperatingSystem to check the current OS
dotnet_diagnostic.MA0144.severity = none
# MA0145: Signature for [UnsafeAccessorAttribute] method is not valid
dotnet_diagnostic.MA0145.severity = none
# MA0146: Name must be set explicitly on local functions
dotnet_diagnostic.MA0146.severity = none
# MA0147: Avoid async void method for delegate
dotnet_diagnostic.MA0147.severity = none
# MA0148: Use pattern matching instead of equality operators for discrete value
dotnet_diagnostic.MA0148.severity = none
# MA0149: Use pattern matching instead of inequality operators for discrete value
dotnet_diagnostic.MA0149.severity = none
# MA0150: Do not call the default object.ToString explicitly
dotnet_diagnostic.MA0150.severity = none
# MA0151: DebuggerDisplay must contain valid members
dotnet_diagnostic.MA0151.severity = none
# MA0152: Use Unwrap instead of using await twice
dotnet_diagnostic.MA0152.severity = none
# MA0153: Do not log symbols decorated with DataClassificationAttribute directly
dotnet_diagnostic.MA0153.severity = none
# MA0154: Use langword in XML comment
dotnet_diagnostic.MA0154.severity = none
# MA0155: Do not use async void methods
dotnet_diagnostic.MA0155.severity = none
# MA0156: Use 'Async' suffix when a method returns IAsyncEnumerable<T>
dotnet_diagnostic.MA0156.severity = none
# MA0157: Do not use 'Async' suffix when a method does not return IAsyncEnumerable<T>
dotnet_diagnostic.MA0157.severity = none
# MA0158: Use System.Threading.Lock
dotnet_diagnostic.MA0158.severity = none
# MA0159: Use 'Order' instead of 'OrderBy'
dotnet_diagnostic.MA0159.severity = none
# MA0160: Use ContainsKey instead of TryGetValue
dotnet_diagnostic.MA0160.severity = none
この設定ファイルを各プロジェクトに読み込ませます。Directory.Build.props
または各ライブラリ .csproj
に以下を追加します。
<ItemGroup>
<GlobalAnalyzerConfigFiles Include="Meziantou.Analyzer.globalconfig" />
</ItemGroup>
BannedApiAnalyzers コード分析の設定
InvariantCulture
と Ordinal, OrdinalIgnoreCase
以外を使用できないように禁止します。
Microsoft.CodeAnalysis.BannedApiAnalyzers
を NuGet から導入してください。
次に BannedApiAnalyzers.globalconfig
を作成し、以下の設定をします。設定項目が1つのみなので、.editorconfig
に直接設定しても良いでしょう。
# https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/configuration-files#global-analyzerconfig
is_global = true
# https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.BannedApiAnalyzers/Microsoft.CodeAnalysis.BannedApiAnalyzers.md
# RS0030: 禁止 API を使用しない
dotnet_diagnostic.RS0030.severity = error
禁止にする API の設定ファイル BannedSymbols.txt
を作成し、以下の内容を記述します。
P:System.Globalization.CultureInfo.CurrentCulture
P:System.Globalization.CultureInfo.CurrentUICulture
P:System.Globalization.CultureInfo.InstalledUICulture
P:System.Globalization.DateTimeFormatInfo.CurrentInfo
P:System.Globalization.NumberFormatInfo.CurrentInfo
P:System.StringComparer.CurrentCulture
P:System.StringComparer.CurrentCulture
P:System.StringComparer.CurrentCultureIgnoreCase
P:System.StringComparer.InvariantCulture
P:System.StringComparer.InvariantCultureIgnoreCase
F:System.StringComparison.CurrentCulture
F:System.StringComparison.CurrentCultureIgnoreCase
F:System.StringComparison.InvariantCulture
F:System.StringComparison.InvariantCultureIgnoreCase
M:System.FormattableString.CurrentCulture
過激派は string.Join
と StringBuilder
を追加したこちらをどうぞ。
P:System.Globalization.CultureInfo.CurrentCulture
P:System.Globalization.CultureInfo.CurrentUICulture
P:System.Globalization.CultureInfo.InstalledUICulture
P:System.Globalization.DateTimeFormatInfo.CurrentInfo
P:System.Globalization.NumberFormatInfo.CurrentInfo
P:System.StringComparer.CurrentCulture
P:System.StringComparer.CurrentCulture
P:System.StringComparer.CurrentCultureIgnoreCase
P:System.StringComparer.InvariantCulture
P:System.StringComparer.InvariantCultureIgnoreCase
F:System.StringComparison.CurrentCulture
F:System.StringComparison.CurrentCultureIgnoreCase
F:System.StringComparison.InvariantCulture
F:System.StringComparison.InvariantCultureIgnoreCase
M:System.FormattableString.CurrentCulture
M:System.String.Join(System.Char,System.Object[])
M:System.String.Join`1(System.Char,System.Collections.Generic.IEnumerable{``0})
M:System.String.Join(System.String,System.Object[])
M:System.String.Join`1(System.String,System.Collections.Generic.IEnumerable{``0})
M:System.Text.StringBuilder.Append(System.SByte)
M:System.Text.StringBuilder.Append(System.Byte)
M:System.Text.StringBuilder.Append(System.Int16)
M:System.Text.StringBuilder.Append(System.Int32)
M:System.Text.StringBuilder.Append(System.Int64)
M:System.Text.StringBuilder.Append(System.Single)
M:System.Text.StringBuilder.Append(System.Double)
M:System.Text.StringBuilder.Append(System.Decimal)
M:System.Text.StringBuilder.Append(System.UInt16)
M:System.Text.StringBuilder.Append(System.UInt32)
M:System.Text.StringBuilder.Append(System.UInt64)
M:System.Text.StringBuilder.Append(System.Object)
M:System.Text.StringBuilder.AppendJoin(System.Char,System.String[])
M:System.Text.StringBuilder.AppendJoin(System.Char,System.Object[])
M:System.Text.StringBuilder.AppendJoin`1(System.Char,System.Collections.Generic.IEnumerable{``0})
M:System.Text.StringBuilder.AppendJoin(System.String,System.String[])
M:System.Text.StringBuilder.AppendJoin(System.String,System.Object[])
M:System.Text.StringBuilder.AppendJoin`1(System.String,System.Collections.Generic.IEnumerable{``0})
これらの設定ファイルを各プロジェクトに読み込ませます。Directory.Build.props
または各ライブラリ .csproj
に以下を追加します。
<ItemGroup>
<GlobalAnalyzerConfigFiles Include="BannedApiAnalyzers.globalconfig" />
<AdditionalFiles Include="BannedSymbols.txt" />
</ItemGroup>
詳しくは後述します。
ライブラリ設定向けの設定は、以上で完了です。
InvariantGlobalization について
グローバリゼーション インバリアント モードの設定 で設定した InvariantGlobalization についてです。
.NET 5 から Windows では NLC (National Language Support)
から ICU (International Components for Unicode)
が使用されるように変更されました。
- .NET 5 以降で文字列を比較するときの動作の変更 - .NET | Microsoft Learn
- 破壊的変更: グローバリゼーション API では Windows 10 上の ICU ライブラリが使用される - .NET | Microsoft Learn
- 祝 .NET 5.0 リリース: .NET Core 3.1 からの移行話 | ++C++; // 未確認飛行 C ブログ
- 忘れがちなカルチャー依存問題 | ++C++; // 未確認飛行 C ブログ
- String comparisons are harder than it seems - Meziantou’s blog
各バージョンの既定の動作は、以下のようになります。
Runtime | Windows | Linux | 備考 |
---|---|---|---|
.NET Framework 1.0 - 4.8 | NLS | - | |
NET Core 1.0 - 3.1 | NLS | ICU | OS で動作が異なっていた |
.NET 5.0 | ICU | ICU | ICU に統一された |
実際にソートを試してみます。
var strings = new[]
{
"あ", "ぁ", // ひらがな
"ア", "ァ", // 全角カタカナ
"ア", "ァ", // 半角カタカナ
"A", "a", // 半角
"A", "a", // 全角
};
strings.Order(StringComparer.Ordinal).ToArray();
strings.Order(StringComparer.CurrentCulture).ToArray();
strings.Order(StringComparer.InvariantCulture).ToArray();
IComparer<string>
は以下を指定します。
StringComparer.Ordinal
StringComparer.CurrentCulture (ja-JP)
StringComparer.InvariantCulture
- NLS を有効にした場合の
StringComparer.CurrentCulture (ja-JP)
- NLS を有効にした場合の
StringComparer.InvariantCulture
4 と 5 は ICU の代わりに NLS を使用する を true
にした場合です。.NET Framework と同じ動作になります。
これに IgnoreCase
を加えたものも試してみます。結果が同じ場合は、同じ列にまとめて記載しています。
1. Ordinal | 2. Current (ICU) | 3. Invariant (ICU) | 4. Current (NLS), 5. Invariant (NLS) | 2. Current (ICU), 3. Invariant (ICU) + IgnoreCase | 4. Current (NLS), 5. Invariant (NLS) + IgnoreCase |
---|---|---|---|---|---|
A | a | a | a | A | A |
a | a (全角) | a (全角) | a (全角) | a | a |
ぁ | A | A | A | A (全角) | A (全角) |
あ | A (全角) | A (全角) | A (全角) | a (全角) | a (全角) |
ァ | ぁ | ぁ | ァ (半角カナ) | あ | ァ (半角カナ) |
ア | ァ | あ | ァ | ぁ | ァ |
A (全角) | ァ (半角カナ) | ァ | ぁ | ア | ぁ |
a (全角) | あ | ァ (半角カナ) | ア (半角カナ) | ア (半角カナ) | ア (半角カナ) |
ァ (半角カナ) | ア | ア | ア | ァ | ア |
ア (半角カナ) | ア (半角カナ) | ア (半角カナ) | あ | ァ (半角カナ) | あ |
❗ ここで各カルチャで以下の文字は、同じ文字と判定されています。
CurrentCulture ja-JP (ICU)
- ✅
あ
=ア
=ア
(大文字の “あ” 系) - ✅
ぁ
=ァ
=ァ
(小文字の “ぁ” 系) - ✅
A
=A
(大文字の “A” 系) - ✅
a
=a
(小文字の “a” 系)
- ✅
CurrentCultureIgnoreCase ja-JP (ICU)
- ⚠️
あ
=ぁ
(カタカナの判定が消えた…) - ✅
A
=a
- ✅
A
=a
- ⚠️
InvariantCulture (ICU)
- ✅ 上記の全ての文字は異なると判定されている
InvariantCultureIgnoreCase (ICU)
- ⚠️
あ
=ぁ
(InvariantCulture では区別されていたのに IgnoreCase になると、ひらがなだけ判定される…) - ✅
A
=a
- ✅
A
=a
- ⚠️
なお、Ordinal
と NLS の IgnoreCase
は、英字の大小文字のみを正しく同じと判定されています。
OrdinalIgnoreCase
CurrentCultureIgnoreCase ja-JP (NLS)
InvariantCultureIgnoreCase ja-JP (NLS)
- ✅
A
=a
- ✅
A
=a
- ✅
このように ICU 環境では統一感のない結果になっているため、注意が必要です。
Incorrect string comparison for Japanese small letters with IgnoreCase option in .NET 5 ICU · Issue #54987 · dotnet/runtime
つまり、Ordinal
と OrdinalIgnoreCase
で判定するのが確実であり、最も高速です。
特殊な並び替えが必要な場合は、自分で明示的に並び替えた方が良いでしょう。
例えば、大文字小文字を区別をしない自然ソートがあります。
PredefinedCulturesOnly について
グローバリゼーション インバリアント モードの設定 で設定した PredefinedCulturesOnly についてです。
InvariantGlobalization
を有効にすることでカルチャ依存問題は解決しますが、次の問題が発生します。
破壊的変更: グローバリゼーション インバリアント モードでのカルチャの作成とケース マッピング - .NET | Microsoft Learn
具体的には、以下のようなコードで CultureInfo
が作成された時に CultureNotFoundException
が発生します。
// 1041 は "ja-JP"
new CultureInfo("ja-JP");
new CultureInfo(1041);
CultureInfo.GetCultureInfo("ja-jp")
CultureInfo.GetCultureInfo(1041)
.NET 6 で PredefinedCulturesOnly
という設定が追加されました。これを false
にすることで、例外ではなく InvariantCulture
と同等のカルチャ設定が返されるようになります。
<PropertyGroup>
<InvariantGlobalization>true</InvariantGlobalization>
<PredefinedCulturesOnly>false or true</PredefinedCulturesOnly>
</PropertyGroup>
自分のコードではなく参照した NuGet ライブラリ内で CultureInfo が作成されても例外が発生するため、PredefinedCulturesOnly
を false
に設定することで回避します。
また、int
指定の CultureInfo
の作成時の例外は、.NET 8 で発生しないように修正されました。
PredefinedCulturesOnly=false not respected in new CultureInfo(int) #86878
.NET | PredefinedCulturesOnly | CultureInfo(“ja-JP”) | CultureInfo(1041) |
---|---|---|---|
.NET 5 | - | ✅ InvariantCulture を返す | ❌ CultureNotFoundException 発生 |
.NET 6, .NET 7 | true (既定値) | ❌ CultureNotFoundException 発生 | ❌ CultureNotFoundException 発生 |
.NET 6, .NET 7 | false | ✅ InvariantCulture を返す | ❌ CultureNotFoundException 発生 |
.NET 8 以降 | true (既定値) | ❌ CultureNotFoundException 発生 | ❌ CultureNotFoundException 発生 |
.NET 8 以降 | false | ✅ InvariantCulture を返す | ✅ InvariantCulture を返す |
この設定により、例外が発生することなく、常に既定で InvariantCulture
で動作するようになります。
CultureInfo.DefaultThreadCurrentCulture について
インバリアント カルチャの文字列フォーマットの設定 についての説明です。
DateTimeFormat
の各設定は、ToString()
する際に以下の部分に適用されます。
Init();
var d = new DateTime(2024, 1, 2, 3, 4, 5, DateTimeKind.Unspecified);
d.ToString(); // 2024-01-02 (5) 03:04:05 (2)
d.ToLongDateString(); // 2024-01-02 (1)
d.ToShortDateString(); // 2024-01-02 (5)
d.ToLongTimeString(); // 03:04:05 (2)
d.ToShortTimeString(); // 03:04 (6)
d.ToString("F"); // 2024-01-02 (1) 03:04:05 (2)
d.ToString("G"); // 2024-01-02 (5) 03:04:05 (2)
d.ToString("g"); // 2024-01-02 (5) 03:04:05 (6)
d.ToString("M"); // 01-02 (3)
d.ToString("Y"); // 2024-01 (4)
d.ToString("U"); // 2024-01-01 (1) 18:04:05 (2)
var o = new DateOnly(2024, 1, 2);
o.ToString(); // 2024-01-02 (5)
o.ToLongDateString(); // 2024-01-02 (1)
o.ToShortDateString(); // 2024-01-02 (5)
o.ToString("M"); // 01-02 (3)
o.ToString("Y"); // 2024-01 (4)
var t = new TimeOnly(3,4,5,6,7);
t.ToString(); // 03:04:05 (6)
t.ToLongTimeString(); // 03:04:05 (2)
t.ToShortTimeString(); // 03:04 (6)
static void Init()
{
var c = (CultureInfo)CultureInfo.InvariantCulture.Clone();
c.DateTimeFormat.LongDatePattern = "yyyy'-'MM'-'dd (1)";
c.DateTimeFormat.LongTimePattern = "HH':'mm':'ss (2)";
c.DateTimeFormat.MonthDayPattern = "MM'-'dd (3)";
c.DateTimeFormat.YearMonthPattern = "yyyy'-'MM (4)";
c.DateTimeFormat.ShortDatePattern = "yyyy'-'MM'-'dd (5)";
c.DateTimeFormat.ShortTimePattern = "HH':'mm (6)";
CultureInfo.DefaultThreadCurrentCulture = c;
}
このカスタムした設定と標準の InvariantCulture
の比較をまとめました。
まず、CultureInfo.DateTimeFormat
の設定値の比較です。
DateTimeFormat のプロパティ | カスタム設定 | InvariantCulture | InvariantCulture 出力例 |
---|---|---|---|
(1) LongDatePattern | yyyy'-'MM'-'dd | dddd, dd MMMM yyyy | Tuesday, 02 January 2024 |
(2) LongTimePattern | HH':'mm':'ss | HH:mm:ss | 03:04:05 |
(3) MonthDayPattern | MM'-'dd | MMMM dd | January 02 |
(4) YearMonthPattern | yyyy'-'MM | yyyy MMMM | 2024 January |
(5) ShortDatePattern | yyyy'-'MM'-'dd | MM/dd/yyyy | 01/02/2024 |
(6) ShortTimePattern | HH':'mm | HH:mm | 03:04 |
次に、DateTime
と DateOnly
と TimeOnly
の ToString()
の比較です。
ToString | カスタム設定 | InvariantCulture |
---|---|---|
DateTime.ToString() , ToString("G") | 2024-01-02 03:04:05 | 01/02/2024 03:04:05 |
DateTime.ToLongDateString() , ToString("D") | 2024-01-02 | Tuesday, 02 January 2024 |
DateTime.ToShortDateString() , ToString("d") | 2024-01-02 | 01/02/2024 |
DateTime.ToLongTimeString() , ToString("T") | 03:04:05 | 03:04:05 |
DateTime.ToShortTimeString() , ToString("t") | 03:04 | 03:04 |
DateTime.ToString("F") | 2024-01-02 03:04:05 | Tuesday, 02 January 2024 03:04:05 |
DateTime.ToString("G") | 2024-01-02 03:04:05 | 01/02/2024 03:04:05 |
DateTime.ToString("g") | 2024-01-02 03:04 | 01/02/2024 03:04 |
DateTime.ToString("M") | 01-02 | January 02 |
DateTime.ToString("Y") | 2024-01 | 2024 January |
DateTime.ToString("U") (世界時刻 UTC) | 2024-01-01 18:04:05 | Monday, 01 January 2024 18:04:05 |
DateOnly.ToString() , ToString("d") | 2024-01-02 | 01/02/2024 |
DateOnly.ToLongDateString() , ToString("D") | 2024-01-02 | Tuesday, 02 January 2024 |
DateOnly.ToShortDateString() , ToString("d") | 2024-01-02 | 01/02/2024 |
DateOnly.ToString("M") | 01-02 | January 02 |
DateOnly.ToString("Y") | 2024-01 | 2024 January |
TimeOnly.ToString() , ToString("t") | 03:04 | 03:04 |
TimeOnly.ToLongTimeString() , ToString("T") | 03:04:05 | 03:04:05 |
TimeOnly.ToShortTimeString() , ToString("t") | 03:04 | 03:04 |
上記のように ISO 8601 形式に変更しています。
コード分析のルールについて
.NET コード分析の設定、Meziantou.Analyzer コード分析の設定、BannedApiAnalyzers コード分析の設定 について説明します。
Roslyn コード分析は、以下のようなルールがあります。
グローバリゼーション規則 (コード分析) - .NET | Microsoft Learn
適用 | ルール | 備考 |
---|---|---|
✅ | CA1304: CultureInfo を指定します | string.Compare , ToLower , ToUpper , ResourceManager.GetString などが該当 |
✅ | CA1305: IFormatProvider を指定する | 組み込み型や DateTime の xxx.ToString , xxx.Parse や Convert クラスメソッドなどが該当 |
CA1307: 意味を明確にするために StringComparison を指定する | Contains , Equals , Replace , IndexOf , GetHashCode などが該当 | |
✅ | CA1309: Ordinal StringComparison を使用する | string.Compare , Equals で Ordinal or OrdinalIgnoreCase の指定を強制 |
✅ | CA1310: 正確さのために StringComparison を指定する | string.Compare , CompareTo , IndexOf , StartsWith などが該当 |
✅ | CA1311: カルチャの指定またはインバリアント バージョンの使用 | ToLower , ToUpper は ToXxxInvariant 版を使用させる |
Roslyn のアナライザーだけでは不十分なケースがあるので、Meziantou.Analyzer のルール も適用しています。
適用 | Id | Category | Description | Severity | Is enabled | Code fix |
---|---|---|---|---|---|---|
MA0001 | Usage | StringComparison is missing | ℹ️ | ✔️ | ✔️ | |
✅ | MA0002 | Usage | IEqualityComparer | ⚠️ | ✔️ | ✔️ |
MA0011 | Usage | IFormatProvider is missing | ⚠️ | ✔️ | ❌ | |
MA0021 | Usage | Use StringComparer.GetHashCode instead of string.GetHashCode | ⚠️ | ✔️ | ✔️ | |
MA0074 | Usage | Avoid implicit culture-sensitive methods | ⚠️ | ✔️ | ✔️ | |
✅ | MA0076 | Design | Do not use implicit culture-sensitive ToString in interpolated strings | ℹ️ | ✔️ | ❌ |
✅ | MA0111 | Performance | Use string.Create instead of FormattableString | ℹ️ | ✔️ | ✔️ |
上記だけでは CurrentCulture
が使用できてしまうため、BannedApiAnalyzers のルール で禁止しています。
下記の表でカバーできない部分を禁止するように設定しています。
適用 | Id | Category | Enabled | Severity | CodeFix |
---|---|---|---|---|---|
✅ | RS0030 | ApiDesign | True | ⚠️ | False |
既定の検索と比較の種類 の一覧表をベースに結果をまとめてみました。いくつかのメソッドは追記しています。
“💡 不要” の表記の意味は、既定の動作が Ordinal
なので警告が必要ない箇所を表します。
API | 既定の動作 | CA1304 | CA1305 | CA1307 | CA1309 | CA1310 | CA1311 | Meziantou |
---|---|---|---|---|---|---|---|---|
string.Compare | ⚠️ CurrentCulture | ⚠️ 該当 | ⚠️ 該当 | ⚠️ MA0074 | ||||
string.Compare (ignoreCase) | ⚠️ CurrentCulture | ⚠️ 該当 | ⚠️ 該当 | ⚠️ 該当 | ⚠️ MA0011 | |||
string.CompareOrdinal | ✅ Ordinal | |||||||
string.CompareTo | ⚠️ CurrentCulture | ⚠️ 該当 | ||||||
string.Contains | ✅ Ordinal | 💡 不要 | 💡 不要:ℹ️ MA0001 | |||||
string.EndsWith (char) | ✅ Ordinal | |||||||
string.EndsWith (string) | ⚠️ CurrentCulture | ⚠️ 該当 | ⚠️ MA0074 | |||||
string.Equals | ✅ Ordinal | 💡 不要 | ⚠️ 該当 | 💡 不要:ℹ️ MA0001 | ||||
string.GetHashCode | ✅ Ordinal | 💡 不要 | 💡 不要:ℹ️ MA0001 | |||||
string.IndexOf (char) | ✅ Ordinal | 💡 不要:ℹ️ MA0001 | ||||||
string.IndexOf (string) | ⚠️ CurrentCulture | ⚠️ 該当 | ⚠️ 該当 | ⚠️ MA0074 | ||||
string.IndexOfAny | ✅ Ordinal | |||||||
string.LastIndexOf (char) | ✅ Ordinal | |||||||
string.LastIndexOf (string) | ⚠️ CurrentCulture | ⚠️ 該当 | ⚠️ MA0074 | |||||
string.LastIndexOfAny | ✅ Ordinal | |||||||
string.Replace (char) | ✅ Ordinal | |||||||
string.Replace (string) | ✅ Ordinal | 💡 不要 | 💡 不要:ℹ️ MA0001 | |||||
string.Split | ✅ Ordinal | |||||||
string.StartsWith (char) | ✅ Ordinal | |||||||
string.StartsWith (string) | ⚠️ CurrentCulture | ⚠️ 該当 | ⚠️ MA0074 | |||||
string.ToLower | ⚠️ CurrentCulture | ⚠️ 該当 | ⚠️ 該当 | ⚠️ MA0011 | ||||
string.ToLowerInvariant | ✅ InvariantCulture | |||||||
string.ToUpper | ⚠️ CurrentCulture | ⚠️ 該当 | ⚠️ 該当 | ⚠️ MA0011 | ||||
string.ToUpperInvariant | ✅ InvariantCulture | |||||||
string.Trim | ✅ Ordinal | |||||||
string.TrimEnd | ✅ Ordinal | |||||||
string.TrimStart | ✅ Ordinal | |||||||
string == string | ✅ Ordinal | |||||||
string != string | ✅ Ordinal | |||||||
API | 既定の動作 | CA1304 | CA1305 | CA1307 | CA1309 | CA1310 | CA1311 | Meziantou |
string.Create | ⚠️ CurrentCulture | ⚠️ 該当 | ⚠️ MA0011 | |||||
string.Concat | ⚠️ CurrentCulture | |||||||
string.Format | ⚠️ CurrentCulture | ⚠️ 該当 | ⚠️ MA0011 | |||||
string.Join | ⚠️ CurrentCulture | |||||||
xxx.ToString | ⚠️ CurrentCulture | ⚠️ 該当 | ⚠️ MA0011 | |||||
xxx.Parse | ⚠️ CurrentCulture | ⚠️ 該当 | ⚠️ MA0011 | |||||
xxx.TryParse | ⚠️ CurrentCulture | ⚠️ MA0011 | ||||||
Convert.ToXxx | ⚠️ CurrentCulture | ⚠️ 該当 | ⚠️ MA0011 | |||||
StringBuilder().Append (value) | ⚠️ CurrentCulture | |||||||
StringBuilder().Append (AppendInterpolatedStringHandler) | ⚠️ CurrentCulture | ⚠️ 該当 | ⚠️ MA0011 | |||||
StringBuilder().AppendLine | ⚠️ CurrentCulture | ⚠️ 該当 | ⚠️ MA0011 | |||||
StringBuilder().AppendFormat | ⚠️ CurrentCulture | ⚠️ 該当 | ⚠️ MA0011 | |||||
StringBuilder().AppendJoin | ⚠️ CurrentCulture | |||||||
$"{}" 文字列補間 | ⚠️ CurrentCulture | ⚠️ MA0076 | ||||||
FormattableString.CurrentCulture | ⚠️ CurrentCulture | ⚠️ MA0111 | ||||||
FormattableString.Invariant | ✅ InvariantCulture | ⚠️ MA0111 | ||||||
IComparer<string> 引数あり | ⚠️ Comparer<T>.Default | ⚠️ MA0002 | ||||||
IEqualityComparer<string> 引数あり | ✅ EqualityComparer<T>.Default | ⚠️ MA0002 | ||||||
ResourceManager.GetObject | ⚠️ CurrentUICulture | ⚠️ 該当 | ⚠️ MA0011 | |||||
ResourceManager.GetString | ⚠️ CurrentUICulture | ⚠️ 該当 | ⚠️ MA0011 |
💡 CA1307
と CA1310
の違い
CA1307
:Equals
,Contains
も該当するCA1310
:Equals
,Contains
は既定値がOrdinal
のため、該当しない
💡 CA1307
と MA0001
は不要
- 既定値が
Ordinal
なメソッドに対しても警告が出てしまうため - 他のルールでカバーできるため
❗ CA1305
の注意点
xxx.Parse
は該当するがxxx.TryParse
は該当しない https://github.com/dotnet/roslyn-analyzers/issues/1848- Meziantou.Analyzer のようなアナライザーを導入したり、BannedApiAnalyzers で禁止するなど対策がある
❗ 必ず InvariantCulture
, Ordinal
が保証されるわけではない点に注意
ToString("N0", (IFormatProvider?)null);
のようにIFormatProvider
引数にnull
が指定された場合は、CurrentCulture
が適用される (警告なし)。Order((IComparer<string>?)null);
のようにIComparer<T>
引数にnull
が指定された場合は、Comparer<T>.Default
が適用される (警告なし)。- そもそも引数に
IFormatProvider
やIComparer<T>
が指定なく、内部でCurrentCulture
が適用される (警告なし)。 - 外部ライブラリのメソッドを使用している場合、外部ライブラリ内部でで
CurrentCulture
が適用される場合がある (コード分析対象外)。
検証ソースのメモです。
void Main(string x, string y, int n)
{
string.Compare(x, y); // 1309, 1310, MA0074
string.Compare(x, y, true); // 1304, 1309, 1310, MA0011
string.Compare(x, y, true, CultureInfo.CurrentCulture); // 1309, RS0030
string.Compare(x, y, true, CultureInfo.InvariantCulture); // 1309
string.CompareOrdinal(x, y);
x.CompareTo(y); // 1310
x.CompareTo((object?)null); // 1310
x.Contains('c'); // 1307, MA0001
x.Contains('c', StringComparison.CurrentCulture); // RS0030
x.Contains('c', StringComparison.Ordinal);
x.Contains(y); // 1307, MA0001
x.Contains(y, StringComparison.CurrentCulture); // RS0030
x.Contains(y, StringComparison.Ordinal);
x.EndsWith('c');
x.EndsWith(y); // 1310, MA0001
x.EndsWith(y, StringComparison.CurrentCulture); // RS0030
x.EndsWith(y, StringComparison.Ordinal);
x.EndsWith(y, ignoreCase: true, culture: null);
x.Equals(y); // 1307, 1309, MA0001
x.Equals((object?)null);
x.Equals(y, StringComparison.CurrentCulture); // 1309, RS0030
x.Equals(y, StringComparison.Ordinal);
x.GetHashCode(); // 1307, MA0001
x.GetHashCode(StringComparison.CurrentCulture); // RS0030
x.GetHashCode(StringComparison.Ordinal);
x.IndexOf('c'); // 1307, MA0001
x.IndexOf(y); // 1310. MA0074
x.IndexOf(y, StringComparison.CurrentCulture); // RS0030
x.IndexOf(y, StringComparison.Ordinal);
x.IndexOf(y, 0); // 1310. MA0074
x.IndexOf(y, 0, StringComparison.CurrentCulture); // RS0030
x.IndexOf(y, 0, StringComparison.Ordinal);
x.IndexOf(y, 0, 0); // 1310. MA0074
x.IndexOfAny(['c']);
x.LastIndexOf('c');
x.LastIndexOf(y); // 1310. MA0074
x.LastIndexOf(y, StringComparison.CurrentCulture); // RS0030
x.LastIndexOf(y, StringComparison.Ordinal);
x.LastIndexOf(y, 0); // 1310. MA0074
x.LastIndexOf(y, 0, StringComparison.CurrentCulture); // RS0030
x.LastIndexOf(y, 0, StringComparison.Ordinal);
x.LastIndexOf(y, 0, 0); // 1310. MA0074
x.LastIndexOfAny(['c']);
x.Replace('c', 'd');
x.Replace("aa", "bb"); // 1307, MA0001
x.Replace("aa", "bb", StringComparison.CurrentCulture); // RS0030
x.Replace("aa", "bb", StringComparison.Ordinal);
x.Split('c');
x.Split(y);
x.StartsWith('c');
x.StartsWith(y); // 1310, MA0074
x.StartsWith(y, StringComparison.CurrentCulture); // RS0030
x.StartsWith(y, StringComparison.Ordinal);
x.StartsWith(y, ignoreCase: true, culture: null);
x.ToLower(); // 1304, 1311, MA0011
x.ToLower(CultureInfo.CurrentCulture);
x.ToLower(CultureInfo.InvariantCulture);
x.ToLowerInvariant();
x.ToUpper(); // 1304, 1311, MA0011
x.ToUpper(CultureInfo.CurrentCulture);
x.ToUpper(CultureInfo.InvariantCulture);
x.ToUpperInvariant();
x.Trim();
x.TrimStart();
x.TrimEnd();
var eq = x == y;
var ne = x != y;
string.Create($"{n}"); // MA0076
string.Create(null, $"{n}");
string.Create(CultureInfo.InvariantCulture, $"{n}");
string.Concat(n);
string.Format("{0}", n); // 1305, MA0011
string.Format(default(IFormatProvider), "{0}", n);
string.Join(", ", new[] { n });
x.ToString();
x.ToString(CultureInfo.CurrentCulture); // RS0030
x.ToString(CultureInfo.InvariantCulture);
n.ToString(); // 1305, MA0011
n.ToString("N0"); // 1305, MA0011
n.ToString("N0", (IFormatProvider?)null);
n.ToString(CultureInfo.CurrentCulture); // RS0030
n.ToString(CultureInfo.InvariantCulture);
(default(int?)).ToString(); // MA0011
int.Parse("123"); // 1305, MA0011
int.TryParse("123", out _); // MA0011
Enum.Parse<DayOfWeek>("abc");
Convert.ToString(x); // 1305 (string の ToString() なので警告不要)
Convert.ToInt32(x); // 1305, MA0011
Convert.ToSingle(x); // 1305, MA0011
new StringBuilder().Append(n);
new StringBuilder().Append($"{n}"); // CA1305, MA0011
new StringBuilder().AppendLine($"{n}"); // CA1305, MA0011
new StringBuilder().AppendLine(null, $"{n}");
new StringBuilder().AppendLine(CultureInfo.InvariantCulture, $"{n}");
new StringBuilder().AppendFormat("{0}", n); // CA1305, MA0011
new StringBuilder().AppendFormat(CultureInfo.InvariantCulture, "{0}", n);
new StringBuilder().AppendJoin(", ", new[] { n });
Console.WriteLine($"{n:N0}"); // MA0076
FormattableString.CurrentCulture($"{n}"); // MA0111
FormattableString.Invariant($"{n}"); // MA0111
new string[] { x, y }.OrderBy(x => x).ToArray(); // MA0002
new string[] { x, y }.Order().ToArray(); // MA0002
new int[] { n }.OrderBy(x => x.ToString()).ToArray(); // MA0002
new HashSet<string>(); // MA0002
new Dictionary<string, object>(); // MA0002
new ConcurrentDictionary<string, object>(); // MA0002
new SortedList<string, object>(); // MA0002
new ResourceManager(typeof(object)).GetObject("x"); // 1304, MA0011
new ResourceManager(typeof(object)).GetObject("x", CultureInfo.CurrentCulture); // RS0030
new ResourceManager(typeof(object)).GetObject("x", CultureInfo.InvariantCulture);
new ResourceManager(typeof(object)).GetString("x"); // 1304, MA0011
new ResourceManager(typeof(object)).GetString("x", CultureInfo.CurrentCulture); // RS0030
new ResourceManager(typeof(object)).GetString("x", CultureInfo.InvariantCulture);
Activator.CreateInstance(typeof(string));
Activator.CreateInstance(typeof(string), BindingFlags.Default, null, null, CultureInfo.CurrentCulture); // RS0030
x.AsSpan().IndexOf(y.AsSpan(), StringComparison.CurrentCulture); // RS0030
x.AsSpan().IndexOf(y.AsSpan(), StringComparison.Ordinal);
object obj = x;
Console.WriteLine(obj);
}
あとがき
ライブラリ開発向けのコード分析のルール設定は、やりすぎると泥沼ですね。string.Join
や StringBuilder
もほぼ使えなくなってしまいます…。
ZString もカルチャ指定できないようなので、使用できません。
文字列の比較、ソートについては Ordinal
を指定すべきと思いますが、ToString()
する部分はある程度は許容しても良い気がします。
そもそもライブラリ使用者側がインバリアントモードにするかどうか、CurrentCulture
を自由に設定してもらえばいいわけなので、ライブラリ開発者が頑張りすぎる必要はないかもしれませんね。
感謝
++C++; // 未確認飛行 C ブログ
- IgnoreCase を付けると “つ” と “っ” が Equals 扱いになった
- CultureInfo.DefaultThreadCurrentCulture
- 2023/06/20 - .NET の文字列比較でカルチャー未指定を検知する
- 2023/03/18 - 忘れがちなカルチャー依存問題
- 2021/08/22 - .NET のカルチャー依存 API 問題
- 2020/11/16 - 祝 .NET 5.0 リリース: .NET Core 3.1 からの移行話
ブログ
ドキュメント
破壊的変更について
関連記事
新着記事