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

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

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

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

文字列オブジェクトの生成と扱い方を理解すれば、次章のメモリ領域の学習がスムーズに進みます。また、コンピュータプログラム(すなわちそれを作った新人エンジニアであるあなた)とユーザーのコミュニケーション手段として文字列は非常に重要です。

1. 文字と文字列の違い

C#では、文字(char型)と文字列(String型)は別物です。混同してしまう新人エンジニアもいますが、気をつけましょう。

namespace Chap07;
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)です。

文字は数値として扱える

namespace Chap07;
using System;

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

<実行結果>

12354

これは 'あ' のUnicodeコードポイント(UTF-16での値)を10進数で表したものです。

文字列は内部的に“文字の配列”

英単語「string」には「糸」「ひも」という意味があり、文字が糸で連なるイメージです。
たとえば:

namespace Chap07;
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は内部的には文字データを連ねた構造です。

2. Stringクラス

C#のStringは.NETの参照型であり、System.Stringというクラスのエイリアスです。そのため、using System; だけで文字列をすぐに使えます。

インスタンス化

C#では文字列リテラルを使うのが一般的です。

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

new String("Hello") も書けますが、あまり使いません。文字列リテラルを直接書いたほうが効率も良く、同じリテラルを使い回す「インターン化(interning)」も自動で行われます。

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

参照の比較と内容の比較

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

namespace Chap07;
using System;

public class Example04 {
    public static void run() {
        String str1 = new String("Hello");
        String str2 = new String("Hello");
        Console.WriteLine(str1 == str2);       // true
    }
}

<実行結果>

true

str1str2 は異なるオブジェクトですが、C#の == は文字列の内容を比較するため true

注意: C#で文字列を比較する場合は==もOKですが、他の参照型(後述)では==は参照比較になるので混同しないようにしましょう。

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

C#の String はイミュータブル(immutable)です。一度作った文字列オブジェクトは内部の文字を変更できません。

namespace Chap07;
using System;

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

<実行結果>

Hello World

str1 + " World" の操作で元の "Hello" を書き換えたわけではありません。新しい "Hello World" という文字列が生成され、str1 はそちらを指すようになっただけです。

イミュータブルの影響

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#ではStringBuilderSystem.Text.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 Chap07
using System;

public class Example06 {
    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#研修"

3桁カンマ区切り表示

C#では String.Format、あるいは $"{}"文字列補間ToString("N0") などで書式を指定できます。

namespace Chap07;
using System;

public class Example08 {
    public static void run() {
        Console.WriteLine("Hello\nWorld");
    }
}

<実行結果>

123,456,789
123,456,789
  1. "{0:N0}" は、小数点なしで3桁区切りを入れる書式指定
  2. C#には String.Format のほか、文字列補間 $"{...}"price.ToString("N0") など多彩な書き方があります。

このように、書式設定は大事な機能です。業務システムでは金額を扱うことが多いため、よく使います。

6.インスタンスを作らなくても仕事をしてくれるstaticメソッド

ここで、注目していただきたいのは、メソッドの呼び出し方です。

String.Format("{0:N0}"

Stringは先頭文字が大文字になっています。これは、クラスですね。クラスに属するメソッドということでクラスメソッドまたはstaticメソッドと呼ばれます。本書では以降staticメソッドで統一します。

static】は静的という意味で、反意語は【dynamic】(動的)です。staticはクラスにあらかじめ用意してあるメソッドという意味です。動的に作り出したインスタンスが持つメソッドではないという意味です。

staticメソッドは、クラス名.メソッド名()という形で呼び出すことができます

一方、

str.length()

のようにインスタンスを作ってから、その個々のインスタンスのメソッドを呼び出すのをインスタンスメソッドといいます。インスタンスメソッドは、変数名.メソッド名()という形で呼び出します。インスタンスは先頭が小文字、クラスは先頭が大文字というルールですから見分け方は簡単ですね。使い分けはベテランでも迷うところですから今は気にしなくて大丈夫です。

ここでは、なぜ、staticメソッドとインスタンスメソッドがあるのかを考察してみましょう。

str.length()は、str(その中身は"新人エンジニアのためのC#研修")という文字列自身の長さということですから、インスタンスメソッドがふさわしいのです。インスタンスが変われば文字列の長さも変わりますね。

オブジェクト指向には責務という考え方があります。文字列の文字数は誰(どのインスタンス)が知っているべきかというのが責務の例です。この場合は、個々の文字列が自分の文字数を知っているべきです。オブジェクト指向を一言で片付けると「自分のことは自分でしよう」という考え方といえるからです。

一方、String.format()はintを整形して(この例では)String(123,456,789)を得るメソッドです。この処理にはStringのインスタンスが作られる必然性がありません。インスタンスを生成することは、CPU時間とメモリ容量のムダです。最終的な結果として表示される“123,456,789”というStringのインスタンスが得られれば良いのです。そのため、String.format()はインスタンスではなくクラスに属すると考えて、staticメソッドであるべきなのです。intに対応するStringを生成するのは、個々のインスタンスには関係のない決まりきった内容の処理だからです。(下図参照)

新人エンジニア
インスタンスメソッドとstaticメソッドの違い

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

文字列リテラルの中で特殊な意味を持たせるには、C#ではバックスラッシュ \ を使います(Windows日本語環境では円記号に見える場合もある)。

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

public class Example09 {
    public static void run() {
        Console.WriteLine("Hello\nWorld");
    }
}


<実行結果>

Hello
World

このように \n によって改行が挿入されます。

8. null 参照

C#の参照型変数(String, 配列やクラス)は、どのオブジェクトも参照していない特別な状態として null を代入できます。

namespace Chap07;
using System;

public class Example10 {
    public static void Main() {
        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 と 空文字の違い

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

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

namespace Chap07;
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 を代入すると、オブジェクトを参照しない状態になる。ガーベージコレクションにより解放される(タイミングはランタイム依存)

次回は「8章. メソッドを定義して処理を再利用する」を学んでいきます。

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