前回(コレクション・ジェネリクス)の続き

ディクショナリ(連想配列)

ディクショナリ(連想配列などとも言われる)は配列やリストの一つの一般化であり,添字に一般のデータを許す..NETにおいても,ディクショナリはSystem.Collections.Generic.Dictionary<TKey,TValue>として提供されている.

using System;
using System.Collections.Generic;

class DictionaryExample
{
   static void Main()
   {
       // キーの型がstringでバリューの型がintであるディクショナリの作成
       // 添字として使われるデータの部分をキー
       // それに対応したデータをバリューと呼ぶ.
       // 用語が衝突しているが,式の評価結果である値とは直接は関係はない.
       Dictionary<string, int> dict = new Dictionary<string, int>();

       // 辞書に要素を追加するには Add メソッドを用いる.
       dict.Add( "Chocolate", 2 );
       dict.Add( "Chips",     4 );
       dict.Add( "Candy",     5 );

       // バリューにはキーが添字である配列のようにアクセスできる
       dict["Chips"] = 7;
       Console.WriteLine( "dict[\"Chips\"] = " + dict["Chips"] ); // dict["Chips"] = 7

       Console.WriteLine();
       // ContainsKey メソッドにより,キーを含んでいるか否かを検査できる.
       Console.WriteLine( dict.ContainsKey("Candy") );  // True
       Console.WriteLine( dict.ContainsKey("Banana") ); // False 

       Console.WriteLine();
       // foreach 文では個々の要素は KeyValuePair<string, int> として手に入る
       foreach ( KeyValuePair<string, int> kv in dict )
       {
           Console.WriteLine( kv.Key + " -> " + kv.Value );
       }

       Console.WriteLine();
       // キーのみが必要な場合は Keys プロパティを使う.
       // もちろん,キーからバリューを引けるので バリューにもアクセスできる.
       // なお dict.Keys は Dictionary<string, int>.KeyCollection 型
       foreach ( string k in dict.Keys )
       {
           Console.WriteLine( k + " -> " + dict[k] );
       }

       Console.WriteLine();
       // バリューのみが必要な場合は Values プロパティを使う.
       foreach ( int v in dict.Values )
       {
           Console.WriteLine( "Value: " + v );
       }

       // 新しいキーは添字アクセスでも追加できる
       dict["Banana"] = 2;

       // キーを指定することで,そのキーと関連づけられているバリューの組を辞書から取り除ける.
       dict.Remove("Candy");

       Console.WriteLine();
       foreach ( var kv in dict )
       {
           Console.WriteLine( kv.Key + " -> " + kv.Value );
       }
   }
}

上のプログラムの出力は以下となる.

dict["Chips"] = 7

True
False

Chocolate -> 2
Chips -> 7
Candy -> 5

Chocolate -> 2
Chips -> 7
Candy -> 5

Value: 2
Value: 7
Value: 5

Chocolate -> 2
Chips -> 7
Banana -> 2

上記のコードで使用したDictionary<TKey,TValue>のコンストラクタ,プロパティ,メソッド

Dictionary<TKey,TValue>() キーがTKey,バリューがTValue型であるようなディクショナリを作成するコンストラクタ.
Add(TKey, TValue) キーとバリューの対応を付けを辞書に追加する.キーが辞書にすでに存在していたら例外(後述)発生.

this[TKey]

インデクサー.dict[k] = vkが辞書に存在しない場合はdict.Add(k,v)と同様.存在する場合はバリューをvで上書き. dict[k]を式として使うときはキーが辞書に存在してなければ例外発生.

ContainsKey(TKey) キーが辞書に含まれているか否かを判定する.
Keys キーのコレクションを表すプロパティ.読み出し専用.
Values バリューのコレクションを表すプロパティ.読み出し専用.
Remove(TKey) 指定されたキーと対応するバリューを取り除く.返り値はキーの有無(取り除けたか否か)を表すbool

オブジェクト初期化子(object initializer)

ディクショナリはオブジェクト初期化子やコレクション初期化子(前回参照)を用いることで下記のように初期値を指定することができる.

using System;
using System.Collections.Generic;

class DictionaryExample2
{
   static void Main()
   {
       // オブジェクト初期化子を利用した版
       Dictionary<string, int> dict = new Dictionary<string, int>()
       {
           // この{}で囲まれている部分がオブジェクト初期化子({}を含む)
           ["Chocolate"] = 2, // 注意 ";" でなく ","
           ["Chips"]     = 4,
           ["Candy"]     = 5
       };

       foreach ( KeyValuePair<string, int> kv in dict )
       {
           Console.WriteLine( kv.Key + " -> " + kv.Value );
       }


       // コレクション初期化子を利用した版 
       Dictionary<string, int> dict2 = new Dictionary<string, int>()
       {
           // 以下の括弧内がAddの引数になる
           { "Chocolate", 2 }, 
           { "Chips",     4 }, 
           { "Candy",     5 }
       };

       foreach ( KeyValuePair<string, int> kv in dict2 )
       {
           Console.WriteLine( kv.Key + " -> " + kv.Value );
       }
   }
}

オブジェクト初期化子は上記の new Dictionary<string, int>()の直後の中括弧の部分である.一般に,C#ではオブジェクト生成直後のpublicフィールドやプロパティ,添字への代入を,オブジェクト初期化子を使うことで生成とまとめて行うことができる.

using System;

class Point {
  public double X;
  public double Y;
}

class ObjectInitializerExample
{
    static void Main()
    {
        // オブジェクト初期化子により,XとYに生成後に代入する.
        // 実際に以下は
        //
        //    Point temp = new Point();
        //    temp.X = 2;
        //    temp.Y = 3;
        //    Point p = temp;
        //
        // のように挙動する.
        Point p = new Point() { X = 2, Y = 3 };
        Console.WriteLine( "(" + p.X + ", " + p.Y + ")" );
    }
}

例外とその捕捉

さて,以下のコードはどのように振る舞うだろうか?

using System;
using System.Collections.Generic;

class ExceptionExample
{
   static void Main()
   {
       // 空のディクショナリの作成
       Dictionary<string, int> dict = new Dictionary<string, int>();

       // 存在しないキーへのアクセス
       Console.WriteLine( dict["somekey"] );
   }
}

実際に実行させてみると,以下のような実行時エラーとなる.

Unhandled exception. System.Collections.Generic.KeyNotFoundException: The given key 'somekey' was not present in the dictionary.
   at System.Collections.Generic.Dictionary`2.get_Item(TKey key)
   at ExceptionExample.Main() in /Users/kztk/prog/cs/Materials/Week4/ExceptionExample/Program.cs:line 12

もちろん,このようなエラーはContainsKey(TKey)メソッド等を使って逐一検査すれば防ぐことができる.しかしながら,そうした逐一の検査は煩雑でありなおかつコードの見通しを悪くする.また,問題が起きてしまった場合の処理も共通であることもしばしばである.たとえば,対話的アプリケーションだとエラーを報告しユーザの入力を再度促すことになるだろうし,対話的でないアプリケーションだとユーザフレンドリーなエラーメッセージを吐いて終了することになるだろう.

そのような問題を解決するのが例外処理である.例外は上記の存在しないキーへのアクセスなど様々な原因で投げられる. try-catch文を用いると例外を捕捉することができる.

using System;
using System.Collections.Generic;

class ExceptionExample2
{
   static void Main()
   {
       // 空のディクショナリの作成
       Dictionary<string, int> dict = new Dictionary<string, int>();

       try
       {
           Console.WriteLine("Hi");
           Console.WriteLine( dict["somekey"] );
           Console.WriteLine("Hello");
           Console.WriteLine( dict["anotherkey"] );
           Console.WriteLine("Hello?");
           Console.WriteLine( dict["yetanotherkey"] );
           Console.WriteLine("Hello??");
       }
       catch (KeyNotFoundException e)
       {
           // tryの中で KeyNotFoundException型(あるいはその派生型)の例外が発生したら,以下の処理を実施.
           Console.WriteLine("Caught exception:");
           Console.WriteLine(e);
           Console.WriteLine("---------------------------");
           Console.WriteLine("e.GetType().Name: " + e.GetType().Name );
           Console.WriteLine("e.Message: " + e.Message );
           Console.WriteLine("e.Source: " + e.Source );
           Console.WriteLine("e.TargetSite: " + e.TargetSite );
           Console.WriteLine("e.StackTrace:");
           Console.WriteLine( e.StackTrace );
       }
       // 例外が発生し捕捉された or 発生しなかったならばここに処理が移る.
       Console.WriteLine("Hello, again.");
   }
}

上記プログラムを実行すると以下の出力が得られる.

Hi
Caught exception:
System.Collections.Generic.KeyNotFoundException: The given key 'somekey' was not present in the dictionary.
   at System.Collections.Generic.Dictionary`2.get_Item(TKey key)
   at ExceptionExample2.Main() in /Users/kztk/prog/cs/Materials/Week4/ExceptionExample2/Program.cs:line 14
---------------------------
e.GetType().Name: KeyNotFoundException
e.Message: The given key 'somekey' was not present in the dictionary.
e.Source: System.Private.CoreLib
e.TargetSite: Void ThrowKeyNotFoundException[T](T)
e.StackTrace:
   at System.Collections.Generic.Dictionary`2.get_Item(TKey key)
   at ExceptionExample2.Main() in /Users/kztk/prog/cs/Materials/Week4/ExceptionExample2/Program.cs:line 14
Hello, again.

例外を投げるにはthrow文を使用する.

using System;

class Student
{
    private int score = 0;
    public int Score {
       get { return score;  }
       set {
           if (0 <= value && value <= 100)
           {
               score = value;
           }
           else
           {
               // 例外を投げる.
               throw new ArgumentException("Scoreは0から100までの範囲内の整数でなければならない");
           }
       }
    }
    public string Name { get; set; } = "";
}

class ExceptionExample3
{
    static void Main()
    {
       try
       {
          Student s = new Student () { Name = "Taro Tohoku", Score = 105 } ;
       }
       catch(ArgumentException e)
       {
          Console.WriteLine(e);
       }
    }
}

上記のコードの実行結果は以下となる.

System.ArgumentException: Scoreは0から100までの範囲内の整数でなければならない
   at Student.set_Score(Int32 value) in /Users/kztk/prog/cs/Materials/Week4/ExceptionExample3/Program.cs:line 16
   at ExceptionExample3.Main() in /Users/kztk/prog/cs/Materials/Week4/ExceptionExample3/Program.cs:line 29

一般的には例外とは,System.Exceptionを継承しているクラスのオブジェクトである.そのため,System.Exceptionかその派生クラス(上記ArgumentExceptionKeyNotFoundExceptionなど)を継承することでユーザ定義の例外を作成することもできる.

また,一つのtryはいくつものcatchブロックを持つことができる.

try
{
   // s や dict が適当に(nullでないオブジェクトとして)定義されているとする
   s.Score = dict[s.Name];
   Console.WriteLine(s.Score);
}
catch(ArgumentNullException e)
{
    // この具体例においては,s.Name がnullの場合に発生
    // なお,ArgumentNullException は ArgumentException の派生クラス

    // ArgumentNullException が起きたときの処理
}
catch (KeyNotFoundException e)
{
    // この具体例においては,dictがキーs.Nameを含んでいないときに発生
    // なお,KeyNotFoundException は Exception の(直接ではないが)派生クラス

    // KeyNotFoundException が起きたときの処理
}
catch(ArgumentException e)
{
   // この具体例においては,dict[s.Name] が0〜100の範囲にないときに発生
   // なお,ArgumentException は Exception の(直接ではないが)派生クラス

   // ArgumentException が起きたときの処理
}
catch(Exception e)
{
   // この具体例では一見発生しなさそうに見えるが,WriteLine がIOExceptionを投げる可能性がある.
   // 私は未確認だが,標準出力が/dev/fullにリダイレクトされている場合?
   // (参考: https://blog.sunfishcode.online/bugs-in-hello-world/ )

   // Exceptionが起きたときの処理
}

このとき,より特定(より派生側の)の例外に対するcatchをより先頭に書く.そうでない場合はC#ではコンパイルエラーとなる.もし,そのような場合が許されたならば,最初のcatchで例外が捕捉されてしまうので,後段のcatchが使われなくなる.

ファイル入出力

ここではテキストファイルについてのみ紹介する.

Important

以下においてMyText.txt.csprojファイルと同じフォルダ(プロジェクトルート)直下に置くことを想定している. Visual Studioを使っている方は設定が必要になる.

プログラム中の相対パス(./MyText.txt等)の基準は作業フォルダ(作業ディレクトリ)である.作業フォルダは環境設定1で紹介した方法でプロジェクトを作成し・プログラムを実行しているのならば,VS Codeで開いたフォルダ(プロジェクトルート,つまり.csprojファイルがあるフォルダ)が作業フォルダになるはずだ.なので,相対パス./MyText.txtはVS Codeで開いたフォルダ直下に置かれたMyText.txtを指す.

作業フォルダがどこなのかを確認するには,staticプロパティである

Environment.CurrentDirectory

の値を調べればよい.

なお,もう少し正確に言えばコマンドラインからdotnet runを実行する場合は,プロジェクトルートでなくそのコマンドを実行したフォルダが作業フォルダになる.

Visual Studioを利用している場合は特に何も設定しなければ実行ファイルがあるフォルダが作業フォルダとなる.これはさすがに不便だと思うので,必要に応じて.csprojの<PropertyGroup>...</PropertyGroup>内に以下の行を追加することで作業フォルダを.csprojファイルのあるフォルダとするのがよいだろう.

<StartWorkingDirectory>$(MSBuildProjectDirectory)</StartWorkingDirectory>

Visual Studioで.csprojファイルを編集するには「ソリューションエクスプローラー」/「ソリューション」で当該プロジェクト名を右クリックし「プロジェクト ファイルの編集」を選ぶ.

ファイルからの入力

System.IO.FileのstaticメソッドReadAllTextReadAllLinesを利用したり,OpenTextして得られるSystem.IO.StreamReaderオブジェクトを利用したりする方法がある(他にもある).

using System;
using System.IO;

class FileInputExample
{
    static void Main()
    {
        // いろんな方法でファイルの中身を標準出力に出力する.

        string fileToOpen = "./MyText.txt";

        try
        {
            // ファイルの内容を一気に,一個のstringとして読みとる.
            // ファイルが小さい,またはどうせ一度は全部読む必要があるのなら.
            string txt = File.ReadAllText(fileToOpen);
            Console.WriteLine("一気に全部読む(結果を文字列に)");
            Console.WriteLine(txt);

            Console.WriteLine("一気に全部読む(結果を文字列の配列に)");
            // ファイルの内容を一気に,各行のstringの配列として読みとる(各行の終端の改行文字は含まれない)
            // ファイルが小さい,またはどうせ一度は全部読む必要があるのなら.
            string[] lines = File.ReadAllLines(fileToOpen);
            foreach (var s in lines)
            {
                // 読みこんだ行を表示する.
                Console.WriteLine(s);
            }

            Console.WriteLine("一行ごと処理");
            // (utf-8の)テキストファイルを開く
            StreamReader sr = File.OpenText(fileToOpen);
            try
            {
                // ファイルの中身を一行ずつ処理する.ファイルが大きいときなどメモリ使用量を気にするとき.
                string? s; // string?はstringのnull許容版.sr.ReadLine()の返り値の型はstring? 
                while ((s = sr.ReadLine()) != null)
                {
                    Console.WriteLine(s);
                }
            }
            finally
            {
                // finallyブロックは例外が発生した場合もしない場合も実行される

                // 開いたファイルを閉じる.srがGCされる際にも閉じられるが,
                // 同時に開けるファイルの数は有限なので,不要になったらすぐに閉じるのがよい
                sr.Dispose();
            }

            Console.WriteLine("一行ごと処理(usingを使用)");
            // using文を使うともっと簡潔に書ける
            using (StreamReader sr2 = File.OpenText(fileToOpen))
            {
                // この中で例外が発生した場合もそうでない場合も sr2.Dispose() が呼ばれる
                while (sr2.Peek() > -1) // Peek を使ったほうが上に比べてトリッキーでないかも
                {
                    Console.WriteLine(sr2.ReadLine());
                }
            }
        }
        catch (FileNotFoundException e)
        {
            // FileNotFoundExceptionのオブジェクトの FileName プロパティは見付からなかったファイル名を保持
            // https://docs.microsoft.com/en-us/dotnet/api/system.io.filenotfoundexception?view=net-6.0
            // 日本語版 https://docs.microsoft.com/ja-jp/dotnet/api/system.io.filenotfoundexception?view=net-6.0
            Console.WriteLine("ファイル '" + e.FileName + "'が見付かりません");
        }
        // 他の例外は捕捉しない
    }
}

MyText.txt の中身

りんご
オレンジ
バナナ
みかん

出力結果

一気に全部読む(結果を文字列に)
りんご
オレンジ
バナナ
みかん
一気に全部読む(結果を文字列の配列に)
りんご
オレンジ
バナナ
みかん
一行ごと処理
りんご
オレンジ
バナナ
みかん
一行ごと処理(usingを使用)
りんご
オレンジ
バナナ
みかん

上で使用したメソッドなど

File.ReadAllText(string) static メソッド.与えられたパスのファイルを開いて読んで閉じ,中身をstringとして返す.
File.ReadAllLines(string) static メソッド.与えられたパスのファイルを開いて読んで閉じ,中身を各行毎のstringの配列として返す.
File.OpenText(string) static メソッド.与えられたパスのファイルを開き,そこから読みだすためのStreamReaderオブジェクトを返す
StreamReader.ReadLine() ストリームから一行読み,その文字列を返す.ファイルの末尾なら null を返す.
StreamReader.Peek() ストリームから次に読める文字(charの意味で)を表す整数(int)を返す.ファイルの末尾なら -1を返す.

Note

上記の「パス」は絶対パスでも相対パスでもよい.相対パスは現在の作業ディレクトリ基準.設定 or 取得するにはEnvironment.CurrentDirectoryプロパティを用いる.

using文

C#はGC付きの言語であり,参照されないオブジェクトはいずれ解放される.しかしながら,プログラミングにおいてはメモリ以外にもさまざまなリソースを使用する必要がある場合がある.ファイナライザを利用しGC時にオブジェクトが保有しているリソースを解放することはできるものの,リソースは有限であるため使用後すぐに解放することが望ましい.開いたファイル——に関連付けられたファイル記述子——はそのようなリソースの一つである.

リソースを確保(たとえばファイルを開く)した後の処理中に例外が発生する可能性があり,その場合にもリソースは解放(たとえばファイルを閉じる)されなければならない.その目的にtry-finallyを利用することもできるが,簡潔なのはusing文(usingディレクティブとは違う)を使う方法である.

基本的な構文は大雑把には以下の通りである.

using ( 変数 = リソースを確保する式 )

usingブロック内で例外が発生した場合にもそうでない場合にも導入された変数に対し Dispose() が呼ばれリソースが解放される.

Note

そのため,導入された変数の型はインタフェース System.IDisposable を実装してなければならない.

ファイルへの出力

System.IO.FileのstaticメソッドWriteAllTextWriteAllLinesを利用したり,CreateTextして得られるSystem.IO.StreamWriterオブジェクトを利用したりする方法がある(他にもある).

using System;
using System.IO;

class FileOutputExample
{
    static void Main()
    {
        string[] txts = { "りんご", "オレンジ", "バナナ", "みかん" };

        // txtはtxtsの各要素を"\n"を間に挟んでつなげた文字列
        // つまり,txt = txts[0] + "\n" + txts[1] + "\n" + ... + "\n" + txts[txts.Length - 1]
        string txt = string.Join("\n", txts);

        // 文字列の内容を一気に書く
        File.WriteAllText("./MyText1.txt", txt);

        // 文字列配列の内容を一気に書く
        File.WriteAllLines("./MyText2.txt", txts);

        // 一行ずつ書く
        using (StreamWriter sw = File.CreateText("./MyText3.txt"))
        {
            foreach (var s in txts)
            {
                sw.WriteLine(s);
            }
        }
    }
}

上記プログラムを実行すると,実行したディレクトリにMyText1.txt,MyText2.txt および MyText3.txt というファイルが作成される.いずれも以下のような内容だが,MyText1.txt は最終行に終端の改行がない.

りんご
オレンジ
バナナ
みかん

上で使用したメソッドなど

File.WriteAllText(string,string) static メソッド.与えられたパスのファイルを開いて,与えられた文字列で上書きして閉じる.パスにファイルが存在してなければ作成する.
File.WriteAllLines(string,string[]) static メソッド.与えられたパスのファイルを開いて,各行の中身が与えられた文字列の配列になるように上書きして閉じる.パスにファイルが存在してなければ作成する.
File.CreateText(string) static メソッド.与えられたパスのファイルを上書きモードで開き,そこへ書きこむためのStreamWriterオブジェクトを返す.パスにファイルが存在してなければ作成する.
StreamWriter.WriteLine(string) ストリームへ与えられた文字列を出力する.

いくつかの文字列操作関数

using System;
using System.Collections.Generic;

class StringExample
{
    static void Main(string[] args)
    {
        string[] fruits = { "apple", "orange", "banana" };

        // Join(string s, string[] ss)で配列ssに含まれる各文字列を指定された区切りsで結合できる
        string joined = string.Join("|", fruits );
        Console.WriteLine( joined ); // apple|orange|banana

        Console.WriteLine();

        // Split(char c)を使うと指定の区切り文字cで文字列を分割できる
        string[] separated = joined.Split('|');
        Console.WriteLine("\"" + joined + "\".Split('|'): ");
        showSplit( separated ); 

        // Splitはオーバロードされていて,区切りに使うのは文字列でもよい
        string toSplit = "a--b--  c --d-----e--  --f";
        Console.WriteLine("\"" + toSplit + "\".Split(\"--\")" ); 
        showSplit( toSplit.Split("--") ); 

        // Split() と引数なしで呼び出せば空白(改行や全角スペース含む)で区切る
        Console.WriteLine("Split()の例"); 
        showSplit( "Hello, World.  こんにちは世界\t全角スペース→ ←全角スペース\n二行目(\\r\\n終端)\r\n三行目(\\r終端)\r四行目".Split() );

        Console.WriteLine();

        // StartsWith(string s)で指定された文字列sで始まっているか判定できる        
        Console.WriteLine("\"おはようございます\".StartsWith(\"おはよう\") = " + "おはようございます".StartsWith("おはよう") );
        Console.WriteLine("\"おはようございます\".StartsWith(\"ございます\") = " + "おはようございます".StartsWith("ございます") );

        Console.WriteLine("");

        // IndexOf(string s)で指定された文字列sの最初に出現する位置を取得できる(含まれていない場合は -1)
        Console.WriteLine("\"eieio-eieio\".IndexOf(\"ie\") = " + "eieio-eieio".IndexOf("ie") ); // "eieio-eieio".IndexOf("ie") = 1
        Console.WriteLine("\"eieio-eieio\".IndexOf(\"ff\") = " + "eieio-eieio".IndexOf("ff") ); // "eieio-eieio".IndexOf("ff") = -1


        Console.WriteLine();

        // Substring(int index) で指定された位置以降の文字列を切り出す
        Console.WriteLine( "abcdefghijk".Substring(2) ); // cdefghijk

        // Substring(int index, int length)で指定された位置からlengthだけ文字列を切り出す
        Console.WriteLine( "abcdefghijk".Substring(2, 4) ); // cdef

        try 
        {
            // index + length が元の文字列の長さを超えたら  ArgumentOutOfRangeException が発生
            Console.WriteLine( "abcdefghijk".Substring(2, 400) );             
        }
        catch (ArgumentOutOfRangeException e)
        {
            Console.WriteLine("例外発生: " + e.GetType().Name + ": " + e.Message ); 
        }

        Console.WriteLine("");

        // Trim() で先頭および終端からの連続した空白を削除
        Console.WriteLine( "\"" + " Hello, World.  ".Trim() + "\"" ); // "Hello, World."

        // 空白は違う種類の文字でもよい(unicodeの空白文字ならよく全角空白でもOK)
        // 参考:unicodeの空白文字 https://www.compart.com/en/unicode/category/Zs
        Console.WriteLine( "\"" + " \tHello, World. \r\n ".Trim() + "\"" ); // "Hello, World."
        //                         ↑ 全角空白文字
    }

    static void showSplit(string[] ss) {
        Console.Write("\t"); // タブ文字
        foreach (var s in ss) {
            Console.Write("\"" + s + "\" ");
        }
        Console.WriteLine(); 
    }
}

上の実行結果

apple|orange|banana

"apple|orange|banana".Split('|'):
        "apple" "orange" "banana"
"a--b--  c --d-----e--  --f".Split("--")
        "a" "b" "  c " "d" "" "-e" "  " "f"
Split()の例
        "Hello," "World." "" "こんにちは世界" "全角スペース→" "←全角スペース" "二行目(\r\n終端)" "" "三行目(\r終端)" "四行目"

"おはようございます".StartsWith("おはよう") = True
"おはようございます".StartsWith("ございます") = False

"eieio-eieio".IndexOf("ie") = 1
"eieio-eieio".IndexOf("ff") = -1

cdefghijk
cdef
例外発生: ArgumentOutOfRangeException: Index and length must refer to a location within the string. (Parameter 'length')

"Hello, World."
"Hello, World."

上で使用したstringのメソッドなど

Join(string, string[]) staticメソッド.文字列の配列を与えられた区切りを中に入れて連接する.
Split(char) 文字列を与えられた区切り文字で分割した配列を返す.
Split(string) 文字列を与えられた区切り文字列で分割した配列を返す.
Split() 文字列を空白文字(改行やタブ文字や非ASCII空白も含む)で分割した配列を返す.
StartsWith(string) 文字列が与えられた文字列で始まっているか判定する.
IndexOf(string) 与えられた文字列が文字列中で最初に出現するインデックスを返す.
Substring(int) 与えられたインデックス以降からなる文字列を返す.
Substring(int,int) 与えられたインデックス以降で指定された長さの文字列を返す.
Trim() 文字列の先頭および末尾から連続した空白を取り除いた文字列を返す.

Note

SubstringIndexOfで指定するindexや長さはstringcharの配列として見たときのものである.このことは以下のように絵文字等の扱いで直感に反することがある.

using System;

class StringPitfall
{
  static void Main() {

     Console.WriteLine("😱".Length);              // 2 
     Console.WriteLine("0x{0:X}", (int) "😱"[0]); // 0xD83D 
     Console.WriteLine("0x{0:X}", (int) "😱"[1]); // 0xDE31
  }
}

これは,charがunicodeのコードポイントを表すのに十分でないためである.charは0〜65535の範囲しか表すことができない一方, unicodeのコードポイントの最大値は 0x10FFFF = 1114111 となる.実際に絵文字等のcharの範囲で表わせないコードポイントはcharのペアとして表わされる(参考: UTF-16, an encoding of ISO 10646).

また,コードポイント自体も「文字」とは対応しない場合がある.たとえば,「🇯🇵」という一文字は二つのコードポイント(U+1F1EF U+1F1F5)で表現される.