none
Decimal型の変数値を高速かつ低コストでコピーする方法について RRS feed

  • 質問

  • 開発環境はVB.NET 2008 Professional Editionで、Windows Formsアプリケーションを開発しています。
    OSはWindows10 Professional(64bit版)です

    Decimal型の変数値を、他のDecimal型の変数へコピーする際、どのような処理が最も高速かつ低コストの方法になるでしょうか?

        Dim dec_From As new Decimal(10000009)
        Dim dec_To As Decimal = Decimal.MinValue

        ' 1.dec_Toの値が 10000010(dec_From + 1)になるため不可(※ 補足あります)
        dec_To = New Decimal(dec_From)

        ' 2.値は正しくコピーできますが、一度文字列型へ変換しているので処理コストが高いと考えています
        Call Decimal.Tryperse(dec_From.ToString(), dec_To)

        ' 3.通常時はこれで問題ないのですが、dec_Fromがデータベースに対してSELECT文を発行した結果を格納したDataTableに属する
        '   DataRowのItemだった場合、例外『列XXXXは読み取り専用です』が発生するため使いたくない場合があります
        dec_To = dec_From


    【※ 補足】
    1.の原因も気になったので調べてみたのですが、判明には至りませんでした。。
      ※ 10,000,000を超えると、下1桁が五捨六入(四捨五入ではない)されるようなことだけは分かりました。

    ?New Decimal(New Decimal(10000001)).ToString()
    "10000000"
    ?New Decimal(New Decimal(10000004)).ToString()
    "10000000"
    ?New Decimal(New Decimal(10000005)).ToString()
    "10000000"
    ?New Decimal(New Decimal(10000006)).ToString()
    "10000010"
    ?New Decimal(New Decimal(10000009)).ToString()
    "10000010"


    一度他の数値型へ変換されてから新しいDecimal型としてdec_Toが生成されると思ったのですが、いずれも1加算されるようなことが無かったため謎が深まっています。

    ?CInt(dec_From)
    10000009
    ?CDbl(dec_From)
    10000009.0
    ?CDec(dec_From)
    10000009D
    ?CSng(dec_From)
    10000009.0
    ?CLng(dec_From)
    10000009
    ?Decimal.ToInt16(dec_From)
    'OverFlowException発生
    ?Decimal.ToInt32(dec_From)
    10000009
    ?Decimal.ToInt64(dec_From)
    10000009
    ?Decimal.ToSByte(dec_From)
    'OverFlowException発生
    ?Decimal.ToSingle(dec_From)
    10000009.0
    ?Decimal.ToDouble(dec_From)
    10000009.0
    ?Decimal.ToUInt64(dec_From)
    10000009
    ?Decimal.ToUInt32(dec_From)
    10000009
    ?Decimal.ToOACurrency(dec_From)
    100000090000


    タイトルの方法に加え、1.の原因をご存知でしたらご教示くださいませ、どうぞよろしくお願い致します。


    2018年8月20日 5:20

回答

  • コピーだけなら、これで良いのでは? 参照型ではなく値型なのですし。

    Dim dec_From As Decimal = 元の値
    Dim dec_To   As Decimal = dec_From

    Decimal 型というのは、ざっくり言えば「有効桁数付きの10進小数」です。
    内部的には「96bit 長の符号付整数」+「小数点位置(0~28)」の組み合わせで管理されています。

    たとえば Decimal においては、「1.000」と「1.0」とが別のバイナリ表現で管理されます。(ただしどちらも、値として比較した場合には、Decimal.One と同一視されるように設計されています)

    > Dim dec_From As new Decimal(10000009)

    これは、New Decimal(Int32) なコンストラクタとして処理されます。
    Int32 を受け取る限り、誤差は発生しないはずです。

    > dec_To = New Decimal(dec_From)

    New Decimal(Decimal) というコンストラクタは存在しないため
    New Decimal(Single) のコンストラクタに解釈されます。

    そして dec_To = New Decimal(10000009.0F) が 10000010D となります。
    (Single を受け取るコンストラクタの内部実装は InternalCall のため、この誤差の理由は不明ですが)

    有効桁数まで含めて厳密に取り出したいなら、下記のように書けますが、通常は単純代入で十分でしょう。

    Dim bin As Byte() = Decimal.GetBits(dec_From)
    dec_To = New Decimal(bin)

    • 回答としてマーク matsuda11 2018年8月21日 0:15
    2018年8月20日 6:19
  • そして dec_To = New Decimal(10000009.0F) が 10000010D となります。
    (Single を受け取るコンストラクタの内部実装は InternalCall のため、この誤差の理由は不明ですが)

    mono の実装を確認してみたところ、Single を受け取るコンストラクタは、下記のように ToString(InvaliantCulture) を経由する実装になっていました。.NET Framework が同じ実装なのかどうかはさておき、少なくとも下記は、変換速度の面では劣りそうですね。

    Dim ci = CultureInfo.InvariantCulture
    Dim d = Decimal.Parse(singleValue.ToString(ci), NumberStyles.Float, ci)

    なお、この場合の ToString メソッドは、10000009.0F を "10000009" ではなく "1.000001E+07" へと変換するため、Decimal 値が 10000010D になってしまうという流れのようです。

    # もしもこれが ToString("R") な実装だったとしたら、誤差を減らせたのかな…。

    • 回答としてマーク matsuda11 2018年8月21日 0:15
    2018年8月20日 7:18

すべての返信

  • > DataRowのItemだった場合、例外『列XXXXは読み取り専用です』が発生する

    どういうことでしょうか? 意味が分かりませんでした。

    1 と 2 はその例外を避けるための対応ということですか? それも、どうしてそんなことをするのか、よく分かりませんでした。

    2018年8月20日 5:56
  • XY問題のように見受けられます。本当に解決したいことを見失っていませんか?

    「Decimal型の変数値を高速かつ低コストでコピーする方法」であれば3.の「dec_To = dec_From」で間違いありません。

    「DataRowのItemだった場合、例外『列XXXXは読み取り専用です』が発生する」が本来の解決したい問題ではありませんか?

    2018年8月20日 6:10
  • コピーだけなら、これで良いのでは? 参照型ではなく値型なのですし。

    Dim dec_From As Decimal = 元の値
    Dim dec_To   As Decimal = dec_From

    Decimal 型というのは、ざっくり言えば「有効桁数付きの10進小数」です。
    内部的には「96bit 長の符号付整数」+「小数点位置(0~28)」の組み合わせで管理されています。

    たとえば Decimal においては、「1.000」と「1.0」とが別のバイナリ表現で管理されます。(ただしどちらも、値として比較した場合には、Decimal.One と同一視されるように設計されています)

    > Dim dec_From As new Decimal(10000009)

    これは、New Decimal(Int32) なコンストラクタとして処理されます。
    Int32 を受け取る限り、誤差は発生しないはずです。

    > dec_To = New Decimal(dec_From)

    New Decimal(Decimal) というコンストラクタは存在しないため
    New Decimal(Single) のコンストラクタに解釈されます。

    そして dec_To = New Decimal(10000009.0F) が 10000010D となります。
    (Single を受け取るコンストラクタの内部実装は InternalCall のため、この誤差の理由は不明ですが)

    有効桁数まで含めて厳密に取り出したいなら、下記のように書けますが、通常は単純代入で十分でしょう。

    Dim bin As Byte() = Decimal.GetBits(dec_From)
    dec_To = New Decimal(bin)

    • 回答としてマーク matsuda11 2018年8月21日 0:15
    2018年8月20日 6:19
  • 返信どうもありがとうございます、書き方が悪くて申し訳ありません。

    そもそもは3が原因でしたが、これはDataRowのDataColumnのReadOnly属性を外すことでも解決できております。

    ただ、この解決方法を適用し忘れた場合に例外が発生する可能性があるため、安全かつ低コストかつ確実なDecimal型の値コピー方法が無いか模索していたのですが、

    自力かつネット検索だけでは見付からなかったため質問させて頂いた次第です。

    2018年8月20日 6:24
  • ' 3.通常時はこれで問題ないのですが、dec_Fromがデータベースに対してSELECT文を発行した結果を格納したDataTableに属する
    '   DataRowのItemだった場合、例外『列XXXXは読み取り専用です』が発生するため使いたくない場合があります
    dec_To = dec_From

    dec_From が読み取り専用な列だったとしても、禁止されるのは代入行為であって、その値を取得すること自体は禁止されないと思うのですが…。同様のエラーを再現可能な、具体的なコードを提示することはできますか?

    それと、右辺が DataRow の Item ということは、代入式の左辺が Decimal 型で、右辺が Object 型だということになりますよね。だとしたらそれは、そもそも両辺のデータ型が不一致な状態なのですから、直接代入することは控えたほうが良いでしょう。「Option Strict On」にした状態でコンパイルが通るような状態に見直してみてください。

    2018年8月20日 6:30
  • 返信どうもありがとうございます、書き方が悪くて申し訳ありません。

    SurferOnWww様への返信と重複するため一部割愛させて頂きますが、

    dec_To = dec_Fromが高速かつ低コストでコピーする方法とのこと、どうもありがとうございました。

    2018年8月20日 6:31
  • dec_From 側が ReadOnly だったということは、下記のようなイメージで良いでしょうか。
    少なくとも Option Strict Off の場合は例外にもなりませんでしたし、誤差無く渡されるようですが…代入式の右辺と左辺を取り違えていたりはしないでしょうか?

    Dim dec_From As New Decimal(10000009)
    
    Dim tbl As New DataTable("Tbl")
    tbl.Columns.Add("dec_From", GetType(Decimal))
    tbl.Rows.Add(dec_From)
    tbl.Columns("dec_From").ReadOnly = True
    tbl.AcceptChanges()
    
    Dim dec_To As Decimal
    dec_To = tbl.Rows(0).Item("dec_From")

    Option Strict On の場合には最後の代入行でコンパイルエラーになるので、代わりに下記のように書きかえてやれば OK です。

    Dim dec_To As Decimal = tbl.Rows(0).Field(Of Decimal)("dec_From")
    ※該当列の型が Decimal 以外(String や Integer など)だと変換エラーになるので注意。

    また、この列が DBNull を含む可能性がある場合にはこのように書けます。

    '案1:DBNull なら Nothing が代入される。
    Dim dec_To As Decimal? = tbl.Rows(0).Field(Of Decimal?)("dec_From")

    '案2:DBNull だった場合に、代用値(下記なら -1)をセットする
    Dim dec_To As Decimal = If(tbl.Rows(0).Field(Of Decimal?)("dec_From"), -1D)

    DataRow からのフィールド値を取得するには、上記のように .Field(Of ) 拡張メソッドを使いますが、その逆に DataRow のフィールド値を更新する場合には、.SetField(Of ) 拡張メソッドを利用できます。

    2018年8月20日 6:50
  • > そもそもは3が原因でしたが、これはDataRowのDataColumnのReadOnly属性を外すことでも解決できております。

    やっぱり意味不明です。

    ある DataTable の、ある DataColumn の ReadOnly プロパティが true になっていると言ってます? デフォルトで false なので質問者さんが何かしたのですよね。だとすると、

    > この解決方法を適用し忘れた場合

    というのは解せないですね。

    仮に、ある DataTable の、ある DataColumn の ReadOnly プロパティが true となっていたとして、それで、

    > DataRowのItemだった場合、例外『列XXXXは読み取り専用です』が発生する

    となるとすると DataRow の Item プロパティを使って当該 DataColumn のセルに値を設定しようとする場合だと思いますが、1, 2, 3 いずれも例外の解決にはならないと思うのですが。

    何か大いなる勘違いとかありませんか?

    • 編集済み SurferOnWww 2018年8月20日 6:57 脱字追加
    2018年8月20日 6:54
  • そして dec_To = New Decimal(10000009.0F) が 10000010D となります。
    (Single を受け取るコンストラクタの内部実装は InternalCall のため、この誤差の理由は不明ですが)

    mono の実装を確認してみたところ、Single を受け取るコンストラクタは、下記のように ToString(InvaliantCulture) を経由する実装になっていました。.NET Framework が同じ実装なのかどうかはさておき、少なくとも下記は、変換速度の面では劣りそうですね。

    Dim ci = CultureInfo.InvariantCulture
    Dim d = Decimal.Parse(singleValue.ToString(ci), NumberStyles.Float, ci)

    なお、この場合の ToString メソッドは、10000009.0F を "10000009" ではなく "1.000001E+07" へと変換するため、Decimal 値が 10000010D になってしまうという流れのようです。

    # もしもこれが ToString("R") な実装だったとしたら、誤差を減らせたのかな…。

    • 回答としてマーク matsuda11 2018年8月21日 0:15
    2018年8月20日 7:18
  • 魔界の仮面弁士様、3の件のご指摘と具体的な解決方法をご教示頂きどうもありがとうございます。
    ご指摘のとおり「Option Strict On」でビルドしますとコンパイルエラーが多数発生致し、ご指摘のようにDataRowのItemの取得を
    tbl.Rows(0).Item("dec_From")
    のように記述しておりました。
    ご教示頂きました .Field(Of T)、.SetField(Of T) 拡張メソッドを使用して暗黙の変換が発生しないよう対応していきます。


    例外『列XXXXは読み取り専用です』が発生する件、魔界の仮面弁士様、SurferOnWww様が懸念されているような左辺と右辺の間違えはあませんでしたが、
    ご指摘頂き改めてソースを追ってみたところ原因と思われる箇所を特定することができました。
    メソッドの引数へ直接使用しており、そのメソッドの引数の型がObject型、参照設定がByRefとなっており、
    参照設定をByValへ変更したところ例外発生は解消されました。
    (ByRef定義の際もメソッド内では例外が発生せずメソッドの呼び出し元へ戻ってから例外が発生していたため、メソッドの参照設定を見落としていました)
    メソッドの引数定義、中身がかなりよろしくないことも把握しましたのでこちらも対応致します。

    なお、下記がサンプルコードになります(魔界の仮面弁士様にご提供頂きましたサンプルコードを使用させて頂きました、どうもありがとうございます)

    Dim dec_From As New Decimal(10000009)
    
    Dim tbl As New DataTable("Tbl")
    tbl.Columns.Add("dec_From", GetType(Decimal))
    tbl.Rows.Add(dec_From)
    tbl.Columns("dec_From").ReadOnly = True
    tbl.AcceptChanges()
    
    Dim dec_To As Decimal
    dec_To = Subtract(tbl.Rows(0).Item("dec_From"), 1000)    ' ←ここで例外『列dec_Fromは読み取り専用です』発生
    
    ' 第1引数の参照設定を ByRef → ByValへ変更することで例外『列dec_Fromは読み取り専用です』解消
    Public Shared Function Subtract(ByRef target As Object, _
                                    ByRef dec01 As Object) As Decimal
        Dim result As Decimal = Decimal.Zero
        If (IsNumeric(target)) Then
            If (IsNumeric(dec01)) Then
                result = Decimal.Subtract(target, dec01)
            Else
                result = target
            End If
        ElseIf (IsNumeric(dec01)) Then
            result = Decimal.Subtract(Decimal.Zero, dec01)
        End If
    
        Return result
    End Function
    

    魔界の仮面弁士様、SurferOnWww様、どうもありがとうございます。

    2018年8月20日 8:06
  • 魔界の仮面弁士様、何度もご回答頂きどうもありがとうございます!(一部回答を見過ごしてしまい申し訳ありませんでした。)
    Dim bin As Byte() = Decimal.GetBits(dec_From)
    dec_To = New Decimal(bin)

    また、Monoの実装をご確認頂きまして重ねてどうもありがとうございます、なぜ1000万以上のDecimalをコンストラクタへ渡すと丸められるのかが分かりました。

    2018年8月20日 8:22