C#のファイル操作まとめました

# はじめに
タイトルの通りですが、C#におけるファイル操作をまとめてみました。

C#のファイル操作の情報が少なかったもので・・・

# 環境

* Windows 10 または Windows 11
* C#プログラムをコンパイルできるツール（以下2つの内どちらか）
  * C#コンパイラ（csc.exe または msbuild.exe）
  * Visual Studio 2019（Visual Studio 2022でも問題ありません）

# 公式リファレンス

* [File クラス](https://docs.microsoft.com/ja-jp/dotnet/api/system.io.file?view=net-5.0)
* [StreamReader クラス](https://docs.microsoft.com/ja-jp/dotnet/api/system.io.streamreader?view=net-5.0)
* [StreamWriter クラス](https://docs.microsoft.com/ja-jp/dotnet/api/system.io.streamwriter?view=net-5.0)
* [FileStream クラス](https://docs.microsoft.com/ja-jp/dotnet/api/system.io.filestream?view=net-5.0)

# C#でファイル操作

## フォルダ構成について
こんな感じのフォルダ構成で解説していきます。

```
<お好きな任意フォルダ>   # ここでは D:\ で説明します
├─ Program.cs        # C#ソースファイル
└─ files
    ├─ read_sample.txt    # 読み込むファイル
    └─ subfiles
        └─ write_sample.txt  # 書き込むファイル
```

ここではDドライブ直下に、上記のようなフォルダ構成をして説明していきます。

## 存在確認
### 「ファイル」の存在確認
操作したいファイルがちゃんと存在するかを確認するプログラムです。

```cs:Program.cs
using System;
using System.IO;    // ファイル操作に必要

class Program {
    static void Main(string[] args) {
        string path = @"D:\files\read_sample.txt";
        
        // ファイル sample.txt の存在確認
        if (File.Exists(path)) {
            Console.WriteLine("ファイルは存在します。");
        }
        else {
            Console.WriteLine("ファイルがありません。");
        }
    }
}
```

### 「フォルダ」の存在確認
フォルダが存在するか確認するプログラムです。
なんて事はないです。単に `File` → `Directory` に変わっただけです。

```cs:Program.cs
using System;
using System.IO;    // フォルダ操作に必要

class Program {
    static void Main(string[] args) {
        string path = @"D:\files";
        
        // フォルダ files の存在確認
        if (Directory.Exists(path)) {
            Console.WriteLine("フォルダは存在します。");
        }
        else {
            Console.WriteLine("フォルダがありません。");
        }
    }
}
```

### パスの先頭に `@` をつけてるのは何で？
先頭に `@` をつけると `¥` や `\` をエスケープ（制御文字）ではなく文字列として扱えるようになります。

ちなみに `@` を含めた文字列を「逐語的文字列」といいます。

百聞は一見に如かずってことで、`@` の偉大さを見てください。

```cs
// @ あり      スラッシュ（円）が文字列扱い
string path = @"D:\files\sample.txt";

// @ なし      スラッシュ（円）がエスケープ扱い
string path = "D:\\files\\sample.txt";
```

## ファイル一覧
### どんなファイルがあるか表示する
指定したフォルダにどんなファイルがあるか一覧表示するプログラムです。

```cs:Program.cs
using System;
using System.IO;
using System.Linq;    // EnumerateFiles を使用するのに必要

class Program {
    static void Main(string[] args) {
        string path = @"D:\files";
        
        // カレントフォルダ内のファイル名だけ表示
        var files = Directory.EnumerateFiles(path, "*", SearchOption.TopDirectoryOnly);
        foreach (string file in files) {
            Console.WriteLine(file);
        }
        
        // サブフォルダも含めてファイル名を表示
        files = Directory.EnumerateFiles(path, "*", SearchOption.AllDirectories);
        foreach (string file in files) {
            Console.WriteLine(file);
        }
    }
}
```

列挙体 SearchOption には以下の値が登録されており、各々の意味は次の通りです。

| 値（System.IO.SearchOption） | 意味 |
| --- | --- |
| TopDirectoryOnly | カレントフォルダだけ検索する |
| AllDirectories | サブフォルダも含めてすべて検索する |


### ファイルの数を数える
テキストファイル `*.txt` が何個存在するのか数えてみます。

`D:\files` から確認処理を開始してみようかと思います。

```
<お好きな任意フォルダ>   # ここでは D:\ で説明します
├─ Program.cs
└─ files  # ここから数を数える
    ├─ read_sample.txt  # 1つ目
    └─ subfiles
        └─ write_sample.txt  # 2つ目
```

では、テキストファイルを数えるサンプルプログラムを記載します。

```cs:Program.cs
using System;
using System.IO;
using System.Linq;    // EnumerateFiles を使用するのに必要

class Program {
    static void Main(string[] args) {
        string path = @"D:\files";
        int filenum = 0;
        
        // カレントフォルダだけテキストファイルを数える
        filenum = Directory.EnumerateFiles(path, "*.txt", SearchOption.TopDirectoryOnly).Count();
        Console.WriteLine("Exclue SubDir: " + filenum);
        
        // サブフォルダも含めてテキストファイルを数える
        filenum = Directory.EnumerateFiles(path, "*.txt", SearchOption.AllDirectories).Count();
        Console.WriteLine("Ixclue SubDir: " + filenum);
    }
}
```

## ファイル読み込み
読み込む対象のファイルを `read_sample.txt` としてお話します。

```
<お好きな任意フォルダ>   # ここでは D:\ で説明します
├─ Program.cs
└─ files
    ├─ read_sample.txt    # これを読み込みます
    └─ subfiles
        └─ write_sample.txt
```

まずは何でもいいので `read_sample.txt` に何行か文を記入して保存しましょう。ここでは以下のような7つの文を適当に用意しました。

```txt:read_sample.txt
①楽しくクールでセクシーに。
②今のままではいけないと思います。だからこそ日本は今のままではいけないと思っている。
③Q「処理水の海洋放出に関して…」A「一緒にノドグロ食べましょうよ」
④おぼろげながら浮かんできたんです。46という数字が。
⑤私の中で30年後を考えた時に、30年後の自分は何歳かなと、発災直後から考えていました。
⑥プラスチックの原料って石油なんですよね。意外にこれ、知られていないケースがあるんですけど。
⑦Q「化石燃料脱却へどのように取り組むのか？」A「・・・・・・」
```

### 一度にすべて読み込む
ファイル内のテキストを一度に全部読み込むには、以下2つのメソッドを用いる手段があります。

* File.ReadAllText
* StreamReader.ReadToEnd

これらの違いは、開いた後のファイルを閉じる処理を自動でやってくれるのか否かの違いです。
ですので使い方だけ記します。

#### サンプルプログラム【File.ReadAllText】
File.ReadAllText メソッドを用いたファイル読み込みです。
開いたファイルを閉じる操作は必要ありません。


```cs:Program.cs
using System;
using System.IO;
using System.Text;  // Encoding.GetEncoding を使用するのに必要

class Program {
    static void Main(string[] args) {
        string path = @"D:\files\read_sample.txt";
        
        // ファイル読み込み＆文字化け防止
        var text = File.ReadAllText(path, Encoding.GetEncoding("shift-jis"));
        
        // 1文字ずつ表示
        foreach (var ch in text) {
            Console.WriteLine(ch);
        }
        
        // 一度で全部表示
        Console.WriteLine(text);
    }
}
```

`Encoding.GetEncoding("<文字コード>")` で文字化けを対策しています。

日本語を表示させるには `shift-jis` を指定しましょう。

#### サンプルプログラム【StreamReader.ReadToEnd】
StreamReader.ReadToEnd メソッドを用いたファイル読み込みです。
開いたファイルを閉じる処理を書き込まなければなりません。

```cs:Program.cs
using System;
using System.IO;
using System.Text;  // Encoding.GetEncoding を使用するのに必要

class Program {
    static void Main(string[] args) {
        string path = @"D:\files\read_sample.txt";
        
        // ファイルを開く＆文字化け防止
        StreamReader file = new StreamReader(path, Encoding.GetEncoding("shift-jis"));
        if (file == null) {
            Console.WriteLine("ファイルを開けませんでした。");
            return;
        }
        
        // ファイルを読み込む
        var text = file.ReadToEnd();
        
        // ファイルを閉じる
        file.Close();
        
        // ファイルを閉じてから読み込んだ値を表示
        Console.WriteLine(text);
    }
}
```

ファイルを閉じてから内容を表示するのは、以下の理由です。

* すぐにファイルクローズする方が忘れにくいから。
* 書き込みをする場合、ファイルを開きっぱなしだと元ファイルを壊す可能性があるから。

StreamReader はC言語のファイル操作みたいに、ちゃんと最後はファイルクローズする必要があるそうです。

ちなみに、いちいちファイルクローズするのが面倒臭い場合は `using` ステートメントを使うと効果的です。

```cs:Program.cs
using System;
using System.IO;
using System.Text;  // Encoding.GetEncoding を使用するのに必要

class Program {
    static void Main(string[] args) {
        string path = @"D:\files\read_sample.txt";
        
        // ファイルを開く＆文字化け防止
        using (StreamReader file = new StreamReader(path, Encoding.GetEncoding("shift-jis"))) {
            // ファイルを読み込む
            var text = file.ReadToEnd();
            
            // 表示
            Console.WriteLine(text);
        }
    }
}
```

> #### using ステートメント
>
> 使い終わったリソースを自動的に開放してくれる便利な書き方です。
> 難しく考えずに「開いたら**自動で閉じる**」って理解しましょう。
> using ステートメントを使った方がいい場面はこんな感じ。
>
> * ファイル操作
> * OpenCvSharp のような画像・動画の操作

### 1行ずつ読み込む
ファイル内のテキストを1行ずつ全部読み込むには、以下2つのメソッドを用いる手段があります。

* File.ReadAllText
* StreamReader.ReadToEnd

これらの違いも、開いたファイルを自動で閉じるか否かです。

#### サンプルプログラム【File.ReadAllLines】
開いたファイルは自動的に閉じてくれます。

```cs:Program.cs
using System;
using System.IO;
using System.Text;  // Encoding.GetEncoding を使用するのに必要

class Program {
    static void Main(string[] args) {
        string path = @"D:\files\read_sample.txt";
        
        // ファイル読み込み＆文字化け防止
        var lines = File.ReadAllLines(path, Encoding.GetEncoding("shift-jis"));
        
        // 1行ずつ読み込んで表示
        foreach (var line in lines) {
            Console.WriteLine(line);
        }
    }
}
```

#### サンプルプログラム【StreamReader.ReadLine】
開いたファイルを閉じる処理を書く必要があります。

```cs:Program.cs
using System;
using System.IO;
using System.Text;  // Encoding.GetEncoding を使用するのに必要

class Program {
    static void Main(string[] args) {
        string path = @"D:\files\read_sample.txt";
        
        // ファイルを開く＆文字化け防止
        StreamReader file = new StreamReader(path, Encoding.GetEncoding("shift-jis"));
        if (file == null) {
            Console.WriteLine("ファイルを開けませんでした。");
            return;
        }
        
        // 行末まで１行ずつ読み込む
        while (file.Peek() != -1) {
            Console.WriteLine(file.ReadLine());
        }
        
        file.Close();
    }
}
```

こちらも `using` ステートメントを使うと効果的です。

```cs:Program.cs
using System;
using System.IO;
using System.Text;  // Encoding.GetEncoding を使用するのに必要

class Program {
    static void Main(string[] args) {
        string path = @"D:\files\read_sample.txt";
        
        // ファイルを開く＆文字化け防止
        using (StreamReader file = new StreamReader(path, Encoding.GetEncoding("shift-jis"))) {
            // 行末まで１行ずつ読み込む
            while (file.Peek() != -1) {
               Console.WriteLine(file.ReadLine());
            }
        }
    }
}
```

## ファイル書き込み
書き込む対象のファイルを `write_sample.txt` としてお話します。

```
<お好きな任意フォルダ>   # ここでは D:\ で説明します
├─ Program.cs
└─ files
    ├─ read_sample.txt
    └─ subfiles
        └─ write_sample.txt  # これを書き込みます
```

### 一度にすべて書き込む
書き込みでは一度で書き込むような処理がありませんでした。

もっと調べればあるかもしれませんが、私にはわかりませんでした。

> それっぽいメソッド（File.WriteAllText）があったのですが、結局は1行ずつ書き込んでいました。

### 1行ずつ書き込む
ファイルに1行ずつ書き込むには

* File.WriteAllLines
* StreamWriter.WriteLine

#### サンプルプログラム【File.WriteAllLines】
これが一度にすべて書き込むような感じですね。

```cs:Program.cs
using System;
using System.IO;
using System.Text;
using System.Collections.Generic;   // List<T> クラスを使うのに必要

class Program {
    static void Main(string[] args) {
        // 書き込むファイルの絶対パス
        string path = @"D:\files\subfiles\write_sample.txt";
        
        // 書き込む内容（配列でもList<T>クラスでも可能）
        List<string> text = new List<string> {
            "尾藤：なんか、健康のために、気ィ使ってなんかやってることってあります？",
            "鳥羽：あっ!水素水飲んでます",
            "尾藤：水素水！？こっちみて・・・",
            "鳥羽：えっ！？",
            "尾藤：すごいでしょ",
            "鳥羽：ﾅﾝﾃﾞｽｶｺﾚｰ(棒)",
            "尾藤：パンッパンですよ！パンッパン！",
            "尾藤：空けてみたいでしょォ?？↑↑",
            "鳥羽：うんみたーい！！",
            "尾藤：いきますよぉ～？せぇっの！",
            "＼プシュ～～ッ！／",
            "鳥羽：あ　ぁ　～　！　水　素　の　音　ォ　～　！"
        };
        
        // ファイルに書き込む
        File.WriteAllLines(path, text, Encoding.GetEncoding("shift-jis"));
        
        // 書き込んだファイルを読み込む
        Console.WriteLine(File.ReadAllText(path, Encoding.GetEncoding("shift-jis")));
    }
}
```

#### サンプルプログラム【StreamWriter.WriteLine】
開いたファイルを閉じる処理が必要になります。

```cs:Program.cs
using System;
using System.IO;
using System.Text;
using System.Collections.Generic;   // List<T> クラスを使うのに必要

class Program {
    static void Main(string[] args) {
        // 書き込むファイルの絶対パス
        string path = @"D:\files\subfiles\write_sample.txt";
        
        // 書き込む内容（配列でもList<T>クラスでも可能）
        List<string> text = new List<string> {
            "尾藤：なんか、健康のために、気ィ使ってなんかやってることってあります？",
            "鳥羽：あっ!水素水飲んでます",
            "尾藤：水素水！？こっちみて・・・",
            "鳥羽：えっ！？",
            "尾藤：すごいでしょ",
            "鳥羽：ﾅﾝﾃﾞｽｶｺﾚｰ(棒)",
            "尾藤：パンッパンですよ！パンッパン！",
            "尾藤：空けてみたいでしょォ?？↑↑",
            "鳥羽：うんみたーい！！",
            "尾藤：いきますよぉ～？せぇっの！",
            "＼プシュ～～ッ！／",
            "鳥羽：あ　ぁ　～　！　水　素　の　音　ォ　～　！"
        };
        
        // ファイルを開く＆文字化け防止
        // 第二引数が「true」 → 追加書き込みOK
        // 　　　　　「false」→ 追加書き込みせず、上書きして書き込む
        StreamWriter file = new StreamWriter(path, true, Encoding.GetEncoding("shift-jis"));
        
        foreach (var line in text) {
            file.WriteLine(line);
        }
        
        // ファイルクローズ
        file.Close();
        
        // 書き込んだファイルを読み込む
        Console.WriteLine(File.ReadAllText(path, Encoding.GetEncoding("shift-jis")));
    }
}
```

`using` ステートメントを使って、ファイルクローズ処理を省略することもできます。

```cs:Program.cs
using System;
using System.IO;
using System.Text;
using System.Collections.Generic;   // List<T> クラスを使うのに必要

class Program {
    static void Main(string[] args) {
        // 書き込むファイルの絶対パス
        string path = @"D:\files\subfiles\write_sample.txt";
        
        // 書き込む内容（配列でもList<T>クラスでも可能）
        List<string> text = new List<string> {
            "尾藤：なんか、健康のために、気ィ使ってなんかやってることってあります？",
            "鳥羽：あっ!水素水飲んでます",
            "尾藤：水素水！？こっちみて・・・",
            "鳥羽：えっ！？",
            "尾藤：すごいでしょ",
            "鳥羽：ﾅﾝﾃﾞｽｶｺﾚｰ(棒)",
            "尾藤：パンッパンですよ！パンッパン！",
            "尾藤：空けてみたいでしょォ?？↑↑",
            "鳥羽：うんみたーい！！",
            "尾藤：いきますよぉ～？せぇっの！",
            "＼プシュ～～ッ！／",
            "鳥羽：あ　ぁ　～　！　水　素　の　音　ォ　～　！"
        };
        
        // ファイルを開く＆文字化け防止
        // 第二引数が「true」 → 追加書き込みOK
        // 　　　　　「false」→ 追加書き込みせず、上書きして書き込む
        using (StreamWriter file = new StreamWriter(path, false, Encoding.GetEncoding("shift-jis"))) {
            // write_sample.txt に一行ずつ書き込み
            foreach (var line in text) {
                file.WriteLine(line);
            }
        }
        
        // 書き込んだファイルを読み込む
        Console.WriteLine(File.ReadAllText(path, Encoding.GetEncoding("shift-jis")));
    }
}
```


## ファイル状態操作

### 新規ファイル作成
新規ファイルを作成するプログラムです。
`D:\files\subfiles\new.txt`を作成します。

```cs:Program.cs
using System;
using System.IO;

class Program {
    static void Main(string[] args) {
        // 新規ファイルの絶対パス
        string path = @"D:\files\subfiles\new.txt";
        // 新規ファイル作成
        File.Create(path);
    }
}
```

### 既存ファイル削除
指定したファイルを削除するプログラムです。
`D:\files\subfiles\new.txt` を削除します。

```cs:Program.cs
using System;
using System.IO;

class Program {
    static void Main(string[] args) {
        // 削除するファイルの絶対パス
        string path = @"D:\files\subfiles\new.txt";
        // ファイル削除
        File.Delete(path);
        }
    }
}
```

### 新規フォルダ作成
`D:/files/new_folder` というフォルダを新規作成します。

```
<お好きな任意フォルダ>   # ここでは D:\ で説明します
├─ Program.cs
└─ files
    ├─ read_sample.txt
    ├─ new_folder        # これを作ります
    └─ subfiles
        └─ write_sample.txt
```

```cs:Program.cs
using System;
using System.IO;

class Program {
    static void Main(string[] args) {
        // 新規フォルダの絶対パス
        string path = @"D:\files\new_folder";
        // フォルダ新規作成
        Directory.CreateDirectory(path);
    }
}
```

### 既存フォルダ削除
`D:/files/new_folder` というフォルダを削除します。

```cs:Program.cs
using System;
using System.IO;

class Program {
    static void Main(string[] args) {
        // 削除フォルダの絶対パス
        string path = @"D:\files\new_folder";
        // フォルダ削除
        Directory.Delete(path);
    }
}
```

### 中身だけクリア
既に存在するファイルを残し、中身だけをクリア（削除）するには、以下2つの方法があります。

* File.Create で上書き
* FileStream.SetLength(0)

#### サンプルプログラム【File.Create で上書き】

```cs:Program.cs
using System;
using System.IO;

class Program {
    static void Main(string[] args) {
        string path = @"D:\files\subfiles\write_sample.txt";
        // すでに存在しているファイル名で新規作成
        File.Create(path);
    }
}
```

#### サンプルプログラム【FileStream.SetLength(0)】
こっちだと手違いで新規ファイルを作るリスクをなくせますね。別に大した問題ではないかもしれませんが。

```cs:Program.cs
using System;
using System.IO;

class Program {
    static void Main(string[] args) {
        string path = @"D:\files\subfiles\write_sample.txt";
        using (var fs = new FileStream(path, FileMode.Open)) {
            fs.SetLength(0);
        }
    }
}
```

### 移動
`D:/files/read_sample.txt` を `subfiles` フォルダ下に移動するサンプルプログラムです。

```
<お好きな任意フォルダ>   # ここでは D:\ で説明します
├─ Program.cs
└─ files
    └─ subfiles
        ├─ read_sample.txt    # subfiles 下に移動させます
        └─ write_sample.txt
```

```cs:Program.cs
using System;
using System.IO;

class Program {
    static void Main(string[] args) {
        // 移動するファイルの絶対パス
        string file_path = @"D:\files\read_sample.txt";
        
        // 移動先（ファイル名「read_sample.txt」もしっかり明記）
        string move_to_path = @"D:\files\subfiles\read_sample.txt";
        
        // 移動
        File.Move(file_path, move_to_path);
        
    }
}
```

`File.Move`メソッドの移動先パス（第二引数）に、ファイル名まで書くのを忘れないでおきましょう。

フォルダパスだけだとコンパイルは通っても例外が発生します。

### ファイル名の変更
「移動」と同じくFile.Moveメソッドを使用します。

```
<お好きな任意フォルダ>   # ここでは D:\ で説明します
├─ Program.cs
└─ files
    ├─ セクシー大臣のポエム集.txt  # 元「read_sample.txt」
    └─ subfiles
        └─ write_sample.txt
```

`read_sample.txt` → `セクシー大臣のポエム集.txt` にファイル名を変更するプログラムです。

一つ注意ですが、<font color="red">フォルダパスは変えない</font>でください。フォルダパスを変えると移動になります。

```cs:Program.cs
using System;
using System.IO;

class Program {
    static void Main(string[] args) {
        // 変更元ファイルの絶対パス
        string file_path = @"D:\files\read_sample.txt";
        
        // 変更後のファイルの絶対パス（フォルダパスが同じであること）
        string rename = @"D:\files\セクシー大臣のポエム集.txt";
        
        // 移動
        File.Move(file_path, rename);
        
    }
}
```

### コピー
`D:/files/read_sample.txt` を `subfiles` フォルダにコピーするプログラムです。

```
<お好きな任意フォルダ>   # ここでは D:\ で説明します
├─ Program.cs
└─ files
    ├─ read_sample.txt  # コピー元
    └─ subfiles
        ├─ read_sample.txt    # コピー
        └─ write_sample.txt
```

```cs:Program.cs
using System;
using System.IO;

class Program {
    static void Main(string[] args) {
        // コピー元の絶対パス
        string org_path = @"D:\files\read_sample.txt";
        
        // コピー先の絶対パス（ファイル名も忘れずに！）
        string dict_path = @"D:\files\subfiles\read_sample.txt";
        
        // コピー実行
        File.Copy(org_path, dict_path);
    }
}
```