なぜ、文字列の理解が重要なのか、その理由

この記事では、当社の新人エンジニア研修の参考にC#を解説します。

前回は「配列とコレクション」について学び、プログラミングの基礎知識の講義を終えました。

そこで、今回からは本格的にオブジェクト指向のプログラミングについて学んで行きます。そのためには、オブジェクト指向で開発されたアプリケーションがメモリ領域をどう使うのかを理解することが必須なのですが、まずは身近なオブジェクトの例として、文字と文字列の扱いを学びましょう。

文字列オブジェクトの生成と扱い方を理解すれば、次章のメモリ領域の学習にも役立ちます。また、C#が得意とするデスクトップアプリケーションの開発には、文字列操作や整形処理が不可欠です。それでは、さっそく始めましょう。

1. 文字と文字列の違い

C#では、文字(char型)と文字列(string型)は全く別のデータ型です。混同してしまう方もいますが、気をつけましょう。

namespace Chap08;
using System;

public class Example01 {
    public static void run() {
        char c1 = 'あ';
        string str1 = "あ";

        Console.WriteLine(c1);   // 1文字
        Console.WriteLine(str1); // 文字列
    }
}

<実行結果>



どちらも見た目は同じ「“あ”」ですが、c1 は1文字を表す値型char)であり、str1 は複数文字を扱える参照型string)です。

char型はシングルクォーテーション’’で括り、stringはダブルクォーテーション””で括って記述します。

char型を定義するつもりでダブルクォーテーションで括ると一文字の文字列(string)として扱われてしまいますし、一文字のstringを定義するつもりでシングルクォーテーションで括るとcharとして扱われてしまいます。

また、複数文字を含むstringをシングルクォーテーションで括ると、コンパイルエラーになってしまいます。以下のコードを7行目の後に追記してみてください。

        char c2 = 'あいうえお';

実験

なぜコンパイルエラーになるのでしょうか。隣の人に説明してみてください。

文字は数値として扱える

namespace Chap08;
using System;

public class Example02 {
    public static void run() {
        char c1 = 'あ';
        int a = c1;  // 'あ'をintに変換
        Console.WriteLine(a);
    }
}

<実行結果>

12354

これは 'あ' のUnicodeコードポイント(UTF-16という文字コード'あ'を表す値)を10進数の数値として表示した結果です。char型の変数をint型の変数にキャスト演算子を使わずに代入できていることからも、文字が数字で表現されていることがわかりますね。ここで、文字コードについても触れておきます。

文字コードと文字化けについて

皆さんはコンピュータが文字を扱う時、各文字をどうやって区別しているかを考えたことがあるでしょうか。

コンピュータにとって、人間の文字はただの記号でしかありません。なので、各言語の文字を表現するために、一文字一文字に違う番号を振って区別をしているのです。

アルファベットなら大小26文字しかありませんから、52個の番号があれば事足りますね。しかし日本語はそうはいきません。ひらがな、カタカナ、漢字を合わせれば、何万という数の文字を表現する必要があります。

では、世界中の言語の文字を表現しようとしたらどうなるでしょうか。各言語によって文字も違いますから、文字に振る番号数が膨大になるだろうな、と簡単に想像がつきますね。

コンピュータで文字を扱うために使う番号の体系を「文字コード」や「文字エンコード」と呼びます。人間とコンピュータの付き合いも長くなってきましたから、各国で様々な文字コードが開発されてきており、世界中には何種類もの文字コードが存在します。日本語が使える文字コードだけでも、JISSJISEUC-JPUTF-8UTF-16...とたくさんの種類があります。

文字化けという現象について聞いたことはありますか?ちゃんと書いてあるはずの文章の文字がわけのわからない記号などに化けてしまう現象です。文字化けは、異なる文字コードで書かれた文字列を表示しようとして起こります。

上記の例で言うと、UTF-16では'あ'は12354番の文字ですが、文字コードが違えば当然違う番号になっていますから、そのまま表示すると違う文字や記号に化けてしまうのですね。

文字コードには、一つだけの言語に対応するものと、多言語に対応するためのものの二種類があります。代表的な多言語対応の文字コードが、UTF-8UTF-16です。C#では内部の文字コードとして、UTF-16を採用しており、これは一文字を16ビット(2byte)で表現しています。

文字列は内部的に“文字の配列”として扱われている

英単語「string」には「糸」「ひも」という意味があり、文字が糸で連なるイメージです。
たとえば下記のように、文字の配列をStringクラスのコンストラクタに渡すと、文字列を生成できます。

namespace Chap08;
using System;

public class Example03 {
    public static void run() {
        char[] charArray = ['H', 'e', 'l', 'l', 'o'];
        string str = new String(charArray);
        Console.WriteLine(str);
    }
}

これは、stringが内部的にはcharの配列だからです。

2. Stringクラス

ところで、C#では文字列を定義するとき、大文字から始まるStringではなく、小文字から始まるstringを使うことが殆どです。Java等の他の言語とは違いますね。

ですが、実はstringとはSystem.Stringというクラスのエイリアスなのです。エイリアスとは、別名のことです。なので、using System; だけで文字列が使えるのですね。

大変よく使う変数型なので、他の組み込み型と同じように、newを使わずに気軽に宣言できるよう、配慮されているということでしょう。実際は、stringで定義された文字列は、Stringクラスのインスタンスだということを覚えておきましょう。

インスタンス化

C#で文字列を定義する書き方は、””で括って中に文字列を記述するだけです。これを文字列リテラルと言います。

string str1 = "Hello";
Console.WriteLine(str1);

上記は、new String("Hello") と同じ意味ですが、C#ではnew String(文字列リテラル)の記述は非推奨ですstring s = 文字列リテラルと書いたほうが効率も良く、同じ文字列の使い回しも自動で行われます。これを「インターン化(interning)」と言います。

3. 文字列の内容の比較には == 演算子を使う

参照の比較と内容の比較

string型に対する == 演算子は文字列の内容を比較します。

namespace Chap08;
using System;

public class Example04 {
    public static void Run() {
        string str1 = new String(['H', 'e', 'l', 'l', 'o']);
        string str2 = new String(['H', 'e', 'l', 'l', 'o']);
        Console.WriteLine(str1 == str2);       // true
    }
}

<実行結果>

true

上記のように、new String と記述して文字列を生成すると、二つのstringは全く別のインスタンスになります。なので、str1str2 は異なるオブジェクトですが、C#の == は文字列の内容を比較するため、表示は trueです。

注意: C#で文字列を比較する場合は==でOKですが、他の参照型での==は、オブジェクトのメモリ上の位置(つまり、全く同じインスタンスなのか)の比較になるので混同しないようにしましょう。この件は後の章で詳しく解説します。

4. イミュータブル(不変性)と文字列の連結

C#の string はイミュータブル(immutable)です。イミュータブルとは、「不変な」という意味です。一度作った文字列オブジェクトは内容を変更できません。以下の例を見てください。

namespace Chap08;
using System;

public class Example05 {
    public static void run() {
        string str1 = "Hello";
        str1 += " World"; 
        Console.WriteLine(str1);
    }
}

<実行結果>

Hello World

上記のstr1 += " World" の処理は元の "Hello" を書き換えたわけではありません。新しい "Hello World" という文字列が生成され、str1 はそちらを参照しただけです。

namespace Chap08;
using System;

public class Example06 {
    public static void Run() {
        string s1 = "Hello";
        string s2 = s1;
        Console.WriteLine(object.ReferenceEquals(s1, s2)); // true(同じオブジェクトを参照している)

        s1 = "World";         // ① ← 新しい文字列リテラル
        Console.WriteLine(object.ReferenceEquals(s1, s2)); // ② false
    }
}
  1. 文字列が変更されるのではなく、新しく生成された"World"を参照し直した
  2. s2 はそのまま "Hello" を参照

このように、文字列を変更する度に新しいインスタンスが生成されているのです。ここで、イミュータブルについても触れておきましょう。

イミュータブルなデータ型の種類

C#では、全ての組み込み型がイミュータブルです。参照型では、stringDatetimeなどがイミュータブルなデータ型です。stringは参照型なのにイミュータブルな、ちょっと変わったデータ型だと言えますね。

なぜイミュータブルなのか

ここまで読んだ皆さんには、「どうして値を変更する度にインスタンスを作り直すんだろう。手間だし時間がかかるのでは?」と考える方もいらっしゃるかと思います。ですが、実はそうではないのです。

イミュータブルなデータ型が存在する理由はいくつかありますが、ここでは主な二つをご紹介します。

1.作り直したほうが速い

文字列の変更処理で、文字列長が変更前より長くなる場合を考えてみましょう。もし変更前のメモリ領域に収まらない長さになるのなら、どの道新しく領域を確保して文字列を作り直さなければなりませんね。

ということは、文字列の「変更」処理には、「現在使っている領域に変更後の文字列が入りきるかを判断する」という余計な処理が毎回ついて回ることになります。

今の領域に入るなら領域を使い回す、入らないなら新しく作り直す「変更処理」よりも、無条件で変更後の文字列が入り切る領域を探して新しくインスタンスを生成する処理の方が簡潔で且つパフォーマンスも良いわけです。

2.変更可能にすると、並行処理によって変数の内容が壊れる恐れがある

これは、この講義の最後でお話しする非同期処理と関連が深いのですが、変数の内容を変更できるようにしておくと、同時に動いている複数の処理からの変更要求がバッティングして、内容が破壊されてしまう危険があるのです。

毎回作り直すイミュータブルな変数の方が、並行処理に対しても安全なのですね。

大量の文字列を連結したい場合

文字列連結を大量に繰り返す場合だけは、イミュータブルである弊害が出てきます。文字列の生成と破棄が延々と繰り返されることになって、パフォーマンスが悪化する恐れがあります。

このような処理には、StringBuilderSystem.Text.StringBuilderクラス)を使います。このクラスは、ある程度の大きさのメモリ(バッファと言います)をまず確保して、その中で文字列操作を行いますから、都度インスタンスを作り直す処理に比べると格段にパフォーマンスが良くなります。

大量の文字列の連結処理を実務で使うことは稀だと思いますが、そういう場合に使えるクラスがあるのだ、ということは覚えておいてください。

実験

C#のStringBuilderの使い方をネットで調べて、文字列連結処理を書いてみましょう。

5. string が持つ文字列操作メソッド

C#のstringには、文字列操作のための便利なメソッドが数多く用意されています。サンプルコードを実行して、動作を確認してみましょう。

メソッド / プロパティ説明使用例
Length (プロパティ)文字列の長さを取得str.Length
IndexOf(string)部分文字列が最初に出現する位置を返すstr.IndexOf("Java")
Contains(string)部分文字列が含まれているかを bool で返すstr.Contains("研修")
Replace(string, string)指定文字列を別の文字列に置換str.Replace("エンジニア", "SE")
Substring(int, int)一部の文字列を抜き出すstr.Substring(2,5)
ToUpper()/ToLower()大文字/小文字に変換str.ToUpper()
Trim()前後の空白を除去str.Trim()

サンプル

namespace Chap08
using System;

public class Example07 {
    public static void Run() {
        string str = "新人エンジニアのためのC#研修";

        // 文字列の長さ
        Console.WriteLine(str.Length);
        Console.WriteLine();

        // 部分文字列の検索
        Console.WriteLine(str.IndexOf("C#"));
        Console.WriteLine(str.IndexOf("Python")); // 見つからない -> -1
        Console.WriteLine();

        // 含まれるか?
        Console.WriteLine(str.Contains("研修"));   // True
        Console.WriteLine(str.Contains("Python")); // False
        Console.WriteLine();

        // 部分置換
        string str2 = str.Replace("エンジニア", "SE");
        Console.WriteLine(str2);
    }
}

<実行結果>

15

10
-1

True
False

新人SEのためのC#研修

新人エンジニア
位置の数え方

IndexOf("C#") → 10 (C#が文字列中で見つかった位置。0始まりなので先頭から10番目)

IndexOf("Python") → -1 (見つからない)

Replace("エンジニア", "SE")"新人SEのためのC#研修"

6. 文字列の整形

実務では、金額を三桁のカンマ区切りにしたり、小数点以下の桁数を調整したりと、文字列の書式を様々に整えて表示する機会も多いでしょう。C#では、大きく分けて三つの方法で文字列の整形を行えます。

  1. String.formatを使う方法
  2. C#の文字列補間式を使う方法
  3. 文字列に埋め込む値のToStringメソッドに書式を指定する方法

1.は、String.format書式指定子を含む文字列を引数で渡し、続けて文字列に埋め込む値を順に渡す方法です。

2.は、C#独自の整形機能で、$マークを付けた文字列リテラルに直接書式指定子と値を埋め込む方法です。

3.は、intなどのToStringメソッドに書式指定子を渡して整形した文字列を生成させる方法です。

どの方法でも出力結果は同じです。以下がC#で利用できる書式指定子と、カスタム書式指定子の一覧です。

書式指定子

書式指定子対象
G一般 (General)
N数値 (Number)
F固定小数点数 (Fixed-point)
Pパーセント (Percent)
D10進数 (Decimal)
X16進数の大文字 (Hexadecimal)
x16進数の小文字 (同上)
C通貨 (Currency)

カスタム書式指定子

指定子呼び名動作
0ゼロプレースホルダゼロ桁埋め
#桁プレースホルダ桁数の指定
,桁区切り三桁区切り
.小数点小数点以下の桁数指定
%パーセント%を表示
;セクションの区切り複数書式を指定する区切り文字

3桁カンマ区切りで整数を表示するサンプル

以下のサンプルでは、上記三種類のやり方で三桁のカンマ区切りに数値を整形して表示しています。

namespace Chap08;
using System;

// 三桁カンマ区切り
public class Example08 {
    public static void Run() {
        // 整形する数値
        int val = 123456789;

        // 1.String.formatを使う方法
        string s1 = String.Format("{0:#,0}", val);
        Console.WriteLine(s1);

        // 2.文字列補完式を使う方法
        Console.WriteLine($"{val:#,0}");

        // 3.ToStringに書式指定子を渡す方法
        string s2 = val.ToString( "N0");
        Console.WriteLine(s2);
    }
}

<実行結果>

123,456,789
123,456,789
123,456,789

上記3つのうちどれを採用するかは、場合によって最適な方法を選べば良いのですが、1でも2でもOKな場合に皆さんにお薦めなのは、2の文字列補完式です。ご覧のように、文字列補完式を使う書き方が一番シンプルです。また、この方法を使えば、文字列の中に書式と埋め込む値も同時に記述するので、引数の指定漏れも起きません。例えば、上の例のvalが宣言されていないと、コンパイルエラーになります。

String.formatは、実は可変長引数のメソッドです。つまり、呼び出すときの引数の数が決まっていないので、埋め込む値の引数を書き忘れてもコンパイルエラーにはならず、実行時に例外が発生してしまうのです。

実験

色々な書式指定子の使い方を調べて、三つの方法で整形した文字列をコンソールに出力してみましょう。

7. エスケープシーケンス

文字列リテラルの中でも特殊な意味を持つ文字があります。改行やタブなどです。これらをリテラルに記述するには、C#ではバックスラッシュ \ を使います(Windows日本語環境では円マークに見える場合が多い)。

エスケープシーケンス意味
\n改行 (LF)
\tタブ
\'シングルクォート ' を文字として表示
\"ダブルクォート " を文字として表示
namespace Chap08;
using System;

public class Example09 {
    public static void Run() {
        Console.WriteLine("これがシングルクォートです[\']\nこれがダブルクォートです[\"]\nこれがタブです[\t]");
    }
}

<実行結果>

シングルクォートです[']
ダブルクォートです["]
タブです[ Tab Tab Tab]

このように エスケープシーケンスを使って特殊な文字が出力できます。

8. null とは

C#の参照型変数(string, 配列やクラス)は、どのオブジェクトも参照していない特別な状態として null を代入できます。参照型変数の宣言時、まだ値が決まっていない場合の初期化にはnullを使います。また、下記のように参照を持った変数にnullを代入し、どこからも参照されなくすることで、参照先のインスタンスをガベージコレクションの対象にすることが出来ます。

namespace Chap08;
using System;

public class Example10 {
    public static void Run() {
        string str1 = new String("ABC");
        string str2 = str1;
        str1 = null;
        Console.WriteLine(str2); // "ABC"
        str2 = null;
        Console.WriteLine(str2); // null
    }
}

<実行結果>

ABC

null


  1. "ABC" という文字列は当初 str1str2 が指していましたが、両者を null にすることで、どこからも参照されなくなりました。
  2. C#ではガーベージコレクションにより、参照されなくなったオブジェクトは後でメモリから解放されます(ただし、いつ解放するかはランタイムが決定します)。

null と 空文字の違い

初心者の方は、nullと空文字を混同することがありますので、解説しておきます。

  1. null … 変数がどのオブジェクトも指していない状態)(参照先の実体が無い)
  2. "" (空文字) … 長さ0の文字列を参照している状態(参照先の実体がある)

Webアプリケーションやデータベース操作では null チェックがよく必要になります。String.IsNullOrEmpty(...)String.IsNullOrWhiteSpace(...) のようなヘルパーメソッドも用意されています。

namespace Chap08;
using System;

public class Example11 {
    public static void run() {
        Console.Write("文字列を入力してください: ");
        string userInput = Console.ReadLine();

        // nullチェック
        if (userInput == null) {
            Console.WriteLine("入力がnullです。");
            
        // 空文字チェック
        } else if (userInput.Length == 0) // or userInput == "") {
            Console.WriteLine("入力が空です。");
            
        } else {
            Console.WriteLine("入力された文字列: " + userInput);
        }
    }
}

Console.ReadLine()null を返すことは少ないですが、Webアプリなどでは入力フォームやデータベースから null が返るケースも多いので注意が必要です。


<まとめ:隣の人に正しく説明できたらチェックを付けましょう>

□ 文字と文字列は別物 char (文字1つ) と string (複数文字)

□ C#では文字列の内容比較に == を使う(他の型での==は参照比較に注意)

string には Length, IndexOf, Contains, Replace など便利なメソッドがある

□ 参照型に null を代入すると、オブジェクトを参照しない状態になる。ガーベージコレクションにより解放される(タイミングはランタイム依存)

次回は「オブジェクト指向基礎② クラスとインスタンス」を学んでいきます。

最後までお読みいただきありがとうございます。