String.Trimのことを何も知らなかった話

2023年6月17日

思い込みは怖いという話

超今更なんだと思いますが、今日ふとC#.NETでプログラムを書いている時にString.Trimに引数が渡せることを気付きました。
これまで空白除去にしか使っていなかったので脳みそが勝手に空白除去するメソッドとして認識していました。

というわけで、何も知らないままというのもあれなので、Trimの実装を見てみたいと思います。
確認するバージョンは「.NET Framework 4.8」です。

実装を確認してみる

SystemのString.csを確認してTrimメソッドを探すと、オーバーロードされているTrimメソッドを発見しました。
今回は引数がある方の中身を見ていきます。見やすいように整形しています。

        private const int TrimHead = 0;
        private const int TrimTail = 1;
        private const int TrimBoth = 2;

        [Pure]
        public String Trim(params char[] trimChars) {
            if (null==trimChars || trimChars.Length == 0) {
                return TrimHelper(TrimBoth);
            }
            return TrimHelper(trimChars,TrimBoth);
        }

        [System.Security.SecuritySafeCritical]  // auto-generated
        private String TrimHelper(char[] trimChars, int trimType) {
            //end will point to the first non-trimmed character on the right
            //start will point to the first non-trimmed character on the Left
            int end = this.Length-1;
            int start=0;

            //Trim specified characters.
            if (trimType !=TrimTail)  {
                for (start=0; start < this.Length; start++) {
                    int i = 0;
                    char ch = this[start];
                    for( i = 0; i < trimChars.Length; i++) {
                        if( trimChars[i] == ch) break;
                    }
                    if( i == trimChars.Length) { // the character is not white space
                        break;
                    }
                }
            }

            if (trimType !=TrimHead) {
                for (end= Length -1; end >= start;  end--) {
                    int i = 0;
                    char ch = this[end];
                    for(i = 0; i < trimChars.Length; i++) {
                        if( trimChars[i] == ch) break;
                    }
                    if( i == trimChars.Length) { // the character is not white space
                        break;
                    }
                }
            }

            return CreateTrimmedString(start, end);
        }

        [System.Security.SecurityCritical]  // auto-generated
        private String CreateTrimmedString(int start, int end) {
            //Create a new STRINGREF and initialize it from the range determined above.
            int len = end -start + 1;
            if (len == this.Length) {
                // Don't allocate a new string as the trimmed string has not changed.
                return this;
            }

            if( len == 0) {
                return String.Empty;
            }
            return InternalSubString(start, len);
        }

        [System.Security.SecurityCritical]  // auto-generated
        unsafe string InternalSubString(int startIndex, int length) {
            Contract.Assert( startIndex >= 0 && startIndex <= this.Length, "StartIndex is out of range!");
            Contract.Assert( length >= 0 && startIndex <= this.Length - length, "length is out of range!");            

            String result = FastAllocateString(length);

            fixed(char* dest = &result.m_firstChar)
                fixed(char* src = &this.m_firstChar) {
                    wstrcpy(dest, src + startIndex, length);
                }

            return result;
        }

代入や比較の際に半角空白を入れたり入れなかったり、記載方法がバラバラなのは気になるけどスルーします。

引数で渡された値を先頭末尾から除外したインデックスを保持しておき、Lengthを計算。
その後FastAllocateStringで新しい文字列の領域を確保して、wstrcpyで内容をコピーする作りのようですね。

文字列に変更がない場合は、そのまま値を返し、文字列が全部消えた場合はString.Emptyを返す作りです。

まとめ

普段何気なく使っているメソッドでも仕様をしっかり確認すれば、気付いていないもっと便利な使い方ができるのかもしれません。
こんなの気付いてないの私だけ?と思っていますが。同じ勘違いをしている同志が1人でも居る可能性にかけて記事にします。

C#,雑談

Posted by marimo