lycheejam's tech log

チラ裏のメモ帳 | プログラミングは苦手、インフラが得意なつもり。

C# 文字列結合

概要

前回に引き続き疑似マイナンバー生成機の改修ネタです。

  • forからforeachに変更
  • 「”文字” + "文字"」からStringBuilderに変更
  • StringBuilderを使うメリット

int型配列とチェックデジットを文字列に変換する処理があまりにもダサかったので
書き直しました。そこで新しい学びもあったので備忘録として

前回と前々回の記事はこちら
kitigai.hatenablog.com
kitigai.hatenablog.com

ソース

github.com

修正箇所

Form1で各ボタンクリックイベントに記述していたものを
RandomNum.NumBindingとしてメソッド化しました。

修正前

private void myNumBtn_Click(object sender, EventArgs e) {
     int[] myNumArry = new int[11];
     myNumArry = rn.randNum(11);
     digitResult = rn.chkDigits(myNumArry);
     //テキストボックスにマイナンバー表示
     resultBox.Text = "";    //初期化
     for (int i = 0; i < myNumArry.Length; i++) {
         resultBox.Text = resultBox.Text + myNumArry[i].ToString();
     }
     //チェックデジットも追加
     resultBox.Text = resultBox.Text + digitResult.ToString();
}

本当に単純に乱数を生成してその戻り値であるmyNumArry
forで回して「+」で文字列結合させています。

修正後

とりあえずソース

public string NumBinding(int[] numArry, int checkDigit) {
    var sb = new StringBuilder();
    foreach (var item in numArry) {
        //sb.Append(item);
        sb.Append(item.ToString());
    }
    //住民票コード/個人番号 or 法人番号
    if (numArry.Length == 12) {
        sb.Insert(0, checkDigit.ToString());    //先頭に追加
        //sb.Insert(0, "X");  //先頭に追加されているかテスト

    } else {
        sb.Append(checkDigit.ToString());   //末尾に追加
        //sb.Append("X"); //末尾に追加されているかテスト
    }

    return sb.ToString();
}

引数として生成された乱数配列(numArry)と
乱数配列を素に計算されたチェックデジット(checkDigit)を渡します。

そしてStringBuilderクラスを生成して
Appendプロパティで文字列を作成します。

チェックデジットは個人/法人番号、住民票コードで
先頭に追加か末尾に追加で変動するので判定処理を入れています。

こんなの見りゃわかるよと言う感じですが
StringBuilderをなぜ使うのかです。

簡単に3パターンの修正案

簡単な文字列結合のコードを3パターン載せます。

見た目的には修正前の自分のコードは絶対的なダサさを感じるわけですが
メソッド化を行って下記のように書いてみるとコード自体の行数は変わらないし
可読性と言う観点でもどれも大して変わらないのかなとも思います。

//+= + for
public string NumBinding(int[] numArray, int digit) {
    var numStr = "";
    for (int i = 0; i < numArray.Length; i++) {
        numStr += numArray[i].ToString();
    }
    numStr += digit.ToString();
    return numStr;
}

//+= + foreach
public string NumBinding(int[] numArray, int digit) {
    var numStr = "";
    foreach (var num in numArray) {
        numStr += num.ToString();
    }
    numStr += digit.ToString();
    return numStr;
}

//StringBuilder + foreach
public string NumBinding(int[] numArray, int digit) {
    var sb = new StringBuilder();
    foreach (var item in numArray) {
        sb.Append(item);
    }
    sb.Append(digit.ToString());
    return sb.ToString();
}

ですが、C#では文字列は不変オブジェクトとなっており
文字列の結合を繰り返すたびに新しい文字列のインスタンスが生成されるらしいのです。
その無駄なインスタンスの生成をしないのがStringBuilderクラス

文字列は不変オブジェクト?

不変オブジェクトとはいきなり出てきて何ぞやと言う感じです。

おそらくネットだとこれが一番かみ砕いていてわかりやすかったです。

結論だけ言うと不変オブジェクトのインスタンスを生成したら
そのインスタンスの値を変更することができないってなります。

じゃあ今回みたいに乱数配列をどうやって
文字列結合してるのよって言う話になるんですが
文字に変換された数字を「+=」するたびに新たなインスタンスが生成されています。

0~9の値が順番に格納されている要素数10の配列を「+=」で文字列結合すると
表の様に値が新しいアドレスにコピーされたのち次の数字が結合されます。

変数 変数アドレス
str 1010番地 "0"
str 1020番地 "01"
str 1030番地 "012"
str 1040番地 "0123"
str 1050番地 "01234"
str 1060番地 "012345"
str 1070番地 "0123456"
str 1080番地 "01234567"
str 1090番地 "012345678"
str 1100番地 "0123456789"

これは文字列が不変オブジェクトであるためにそのインスタンスの値を変更できないからです。
その為、結合時に新たな一文字を格納できるメモリ領域を確保して新たなインスタンスを生成します。

なので”0”と"1"の結合を見ると1020番地に2文字分の領域を確保して
値"0"を1010番地から1020番地にコピーしたのち、値”1”を配列から1020番地にコピーして
文字列結合がされたように見えているようです。

こうなると無駄なインスタンスが生成されメモリを無駄に消費してしまうと言うことらしいです。

StringBuilderを使うメリット

StringBuilderを使うことでどんなメリットがあるのか
それは文字列が不変オブジェクトであるがためにメモリが
無駄に消費されてしまうと言う点を解決できることです。

解説はこちらの公式のドキュメントを見た方が早いと思います。

StringBuilderクラスは文字列を格納する領域をあらかじめ決めておき
その領域内で文字列の結合を行います。

なので前述の「+=」を使用した場合の文字列結合で起こる無駄なメモリの消費が起こりません。

また、コードではStringBuilderインスタンス生成時に
引数を省略していますが省略した場合16文字分の領域が既定で割り当てられます。

StringBuilderと+演算子の使い分けに関してはDOBONさんにて詳しく解説されているので
そちらの方が参考になります。

雑感

目次入れてみたんですけど目次って便利ですね。