29
12

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

サムザップAdvent Calendar 2021

Day 7

【トラブル回避】端末依存のC#処理に注意!

Last updated at Posted at 2021-12-07

本記事は、サムザップ Advent Calendar 2021の12/7の記事です。

はじめに

C#の一部APIには、端末の設定によって結果が変わる挙動をするものがあります。
今回はそんな危うい挙動をする処理と、それを端末の設定に依存させないで行う方法を紹介したいと思います。

端末の言語設定に依存した処理

float n = float.Parse("1.5");

これは1.5という文字列をパースしてfloatへ変換する何の変哲もないコードですが、
端末が特定の言語に設定されている場合、以下の例外が投げられて処理は失敗してしまいます。

FormatException: Input string was not in a correct format.

これは小数点の表現がカルチャによって異なることと、デフォルトではカルチャは端末の言語設定を参照していることに依るものです。

[ws][sign] [integral-digits[,]]integral-digits[.[fractional-digits]][e[sign]exponential-digits][ws]

. A culture-specific decimal point symbol.
Single.Parse Method (System) | Microsoft Docs

例えば端末の言語がフランスに設定されている場合、小数点はカンマ(,)で表現されるため、
1.5を浮動小数点数として正しくパースすることが出来ません。

この言語設定への依存は、明示的にカルチャを指定することで解消出来ます。
以下のコードでは、float.Parseメソッドの第2引数へ特定の国/地域に依存しないカルチャ1を指定しています。

float n = float.Parse("1.5", CultureInfo.InvariantCulture);

なお今回はfloatを例に挙げましたが、同じ浮動小数点数のdoubleはもちろん、
DateTime, DateTimeOffsetなどのパースも端末の言語設定の影響を受けます。

Unity特有の事情

ちなみにUnityではAndroid向けにビルドしたアプリで、デフォルトのカルチャがInvariantCultureとなるバグ2がありました。
このバグは修正されており、Unity 2020.1.5f1以降ではちゃんと端末に設定された言語のCultureInfoオブジェクトが返されます。

従って同じAndroid端末・同じ言語設定(例では日本語)で以下のコードを実行した場合、

Debug.Log(CultureInfo.CurrentCulture.DisplayName);

Unity 2019.4.15f1では、

Invariant Language (Invariant Country)

Unity 2020.3.14f1では、

Japanese (Japan)

と異なる結果が出力されます。

もしパース処理などでデフォルトのカルチャがInvariantCultureであることに依存していたコードがある場合、
古いUnityからバージョンアップをする際には注意が必要でしょう。

端末のタイムゾーンに依存した処理

少々わざとらしいコードですが、以下の処理も端末のタイムゾーンによって結果が異なります。

// toの指す日付とfromの差を出力する
var to   = DateTimeOffset.ParseExact("2021-12-03T12:00:00.0000000+09:00", "O", CultureInfo.InvariantCulture);
var from = DateTimeOffset.ParseExact("2021-12-01T00:00:00.0000000+09:00", "O", CultureInfo.InvariantCulture);
var delta = to.Date - from; // to.Dateでtoの日付部分を取得する
Debug.Log(delta.ToString());

例えば端末のタイムゾーンが日本標準時(+09:00)の場合は、想定した通りに2日と出力されますが、

2.00:00:00

中央ヨーロッパ時間(+01:00)に設定していた場合は、2日と8時間と出力されます。

2.08:00:00

何故この差異が生じるかというと、
DateTimeOffsetDateプロパティは元々のタイムゾーンが反映されないのと、

The value of the DateTime.Kind property of the returned DateTime object is always DateTimeKind.Unspecified. It is not affected by the value of the Offset property.
DateTimeOffset.Date Property (System) | Microsoft Docs

DateTimeDateTimeOffsetの減算は、DateTimeDateTimeOffsetに変換されてから行われますが、
その変換の際に端末に設定されたタイムゾーンが採用されてしまうことによります。

If the value of the DateTime.Kind property is DateTimeKind.Local or DateTimeKind.Unspecified, the date and time of the DateTimeOffset object is set equal to dateTime, and its Offset property is set equal to the offset of the local system's current time zone.
DateTimeOffset.Implicit(DateTime to DateTimeOffset) Operator (System) | Microsoft Docs

端末のタイムゾーンが中央ヨーロッパ時間(+01:00)のときの処理を一つ一つ追っていくと、

var to   = DateTimeOffset.ParseExact("2021-12-03T12:00:00.0000000+09:00", "O", CultureInfo.InvariantCulture);
var from = DateTimeOffset.ParseExact("2021-12-01T00:00:00.0000000+09:00", "O", CultureInfo.InvariantCulture);
DateTime a = to.Date;      // 1. 2021-12-03 00:00:00, Kind=Unspecified
DateTimeOffset b = a;      // 2. 2021-12-03 00:00:00+01:00
TimeSpan delta = b - from; // 3. 2021-12-03 00:00:00+01:00 - 2021-12-01 00:00:00+09:00
  1. Dateプロパティで日付部分を表すDateTimeインスタンスを取得(DateTimeKindUnspecified
  2. DateTimeインスタンスのDateTimeOffsetへの暗黙の変換、DateTimeKindUnspecifiedなので、システムのタイムゾーン(+01:00)のオフセットが設定される
  3. 差の計算の日時をUTCに換算すると、2021-12-02 23:00:00 - 2021-11-30 15:00:00 となる

結果、差が2日と8時間になってしまっていることが分かります。

端末のタイムゾーン設定に依らず正しい結果を得たい場合は、
日付を取り出す際に元々のオフセットを指定して、明示的にDateTimeOffsetをインスタンス化する必要があります。

var to   = DateTimeOffset.ParseExact("2021-12-03T12:00:00.0000000+09:00", "O", CultureInfo.InvariantCulture);
var from = DateTimeOffset.ParseExact("2021-12-01T00:00:00.0000000+09:00", "O", CultureInfo.InvariantCulture);
var delta = new DateTimeOffset(to.Date, to.Offset) - from;
Debug.Log(delta.ToString());

これで意図した通りに2日と出力されます。

2.00:00:00

最後に

同じ環境で開発と動作確認を行っている場合、見逃されがちな端末の設定に依存した処理を紹介いたしました。
この記事が堅牢なシステムを構築するための助けになれば幸いです。

参考

  1. CultureInfo.InvariantCulture プロパティ (System.Globalization) | Microsoft Docs

  2. Unity Issue Tracker - [Android] System.Globalization.CultureInfo.CurrentCulture returns Culture Invariant when is being built on Android device

29
12
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
29
12

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?