[C#] 初心者が陥った例外・問題まとめ

#初心者が陥った例外・問題まとめ

C#歴1年未満の私が実装していてハマった例外や問題を自分への戒めを込めてごった煮でまとめてみました。

## RemoveメソッドでLINQの操作元のコレクションを削除するとInvalidOperationExceptionが発生

```cs
            hoges = new List<int> { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };

            var targets = hoges.Where(b => b == 9);

            foreach (var target in targets)
            {
                hoges.Remove(target);
            }
            
```

LINQは遅延評価されるためforeach文が実行される時にはまだ計算されておらず、エラーが生じてしまいます。
 
### 解決策
RemoveAllメソッドを使う。
引数にラムダ式を書くことが出来るので、かなりコードがすっきりします。

```cs
            var hoges = new List<int> { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };

            hoges.RemoveAll(b => b == 9);
```
### 参考
http://taka-say.hateblo.jp/entry/2014/02/27/024822



## foreach文で対象のコレクションを操作するとInvalidOperationExceptionが発生

```cs
            var hoges = new List<int> { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };

            foreach (var hoge in hoges)
            {
                if (hoge < 10)
                {
                    hoges.Remove(hoge);
                }
            }
````
「コレクションが変更されました。列挙操作が実行されないことがあります。」と言われてしまいます。

### 解決策
もしコレクションを操作したい場合は、対象のコレクションのコピーを作成しforeach文に使用します。
RemoveAllメソッドだと、さらに簡単に書けます。

```cs
            var hoges = new List<int> { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };

            var hogeDummys = new List<int>(hoges);

            foreach (var hoge in hogeDummys)
            {
                if (hoge < 5)
                {
                    hoges.Remove(hoge);
                }
            }

            // RemoveAllメソッドを使った場合
            hoges.RemoveAll(b => b < 5);
````

### 参考
http://pchin.hatenablog.com/entry/2013/08/10/074454


## ファイルの書き込みがHDD内のファイルに反映されない問題
StreamWriter.WriteLineメソッドを使っていてHDD内のファイルを確認すると内容が書き込まれていないといった問題が起こりました。

```cs
            var memoryStream = new MemoryStream();
            var streamWriter = new StreamWriter(memoryStream);
            streamWriter.WriteLine("hoge");
```

上記の段階でファイルを確認すると書き込まれていないことが分かります。
これは、Colseメソッドを呼んでいないことが原因です。
WriteLineメソッドはCloseメソッドが呼ばれて初めてHDD内のファイルへ書き込みます。
そのため上記のコードだけでは、書き込まれていないことになります。
（usingを使っていればまず会うことのない現象ですが…）

ただし、ファイルへ書き込みたいがStreamを使い続けたいといった場合があります(私はその1人でした)。
その場合は、Flushメソッドを使うことで解決します。
ただし、Streamの解放は忘れないようにしましょう。

### 解決策
usingを使う。Colseメソッドを呼ぶ。
Streamを使い続けたい場合は、Flushメソッドを使う。

```cs

            var memoryStream = new MemoryStream();
            var streamWriter = new StreamWriter(memoryStream);
            streamWriter.WriteLine("hoge");
            streamWriter.Flush();
```

### 参考
https://blogs.yahoo.co.jp/a32kita/13777668.html

##　Streamを使い続けているとObjectDisposedExceptionが発生

下記のようにStreamWriterをCloseしたときにFileStreamもCloseしていることに気づかず、System.ObjectDisposedExceptionが発生することがあります。

```csharp
        using (var fileStream = new FileStream("file.txt", FileMode.Open))
        {
            using (var streamWriter = new StreamWriter(fileStream))
            {
                streamWriter.WriteLine("hoge");
                // StreamWriterだけでなくFileStreamもCloseされる
            }
            // すでにCloseしているFileStreamを操作しようとするためエラー
            fileStream.Seek(0, SeekOrigin.Begin);    
        }
```

### 解決策
Streamを使い続けるときは、不要になったタイミングでStreamをCloseする。

### 参考
albireoさん、コメントありがとうございます。

## ParseExactメソッドでFormatExceptionが発生

ParseExactメソッドの場合、Parse出来なかったときSystem.FormatExceptionが発生します。


```csharp
                string dateStr = "21/Jul/2017:12:10:03+0900";
                string format = "dd/MMM/yyyy:HH:mm:ss zzz";

                var date = DateTime.ParseExact(dateStr, format, System.Globalization.DateTimeFormatInfo.InvariantInfo, System.Globalization.DateTimeStyles.None)

```
### 解決策
TryParseExactメソッドを用いる。
Parse出来なかったとき

- 戻り値にfalseが返る。
- 第5引数に{0001/01/01 0:00:00}が格納される。

```csharp
                string dateStr = "21/Jul/2017:12:10:03+0900";
                string format = "dd/MMM/yyyy:HH:mm:ss zzz";

                DateTime date;
                var isParse = DateTime.TryParseExact(dateStr, format, System.Globalization.DateTimeFormatInfo.InvariantInfo, System.Globalization.DateTimeStyles.None ,out date);
```
これで例外は発生しなくなります。
ただ、なぜ例外が発生したかはわかりにくいですね。
ParseExactメソッドでもSystem.FormatExceptionが発生すると「文字列は有効な DateTime ではありませんでした。」と言われるだけなので、どこが間違っているか分かりにくいです。
(例だとDateTimeStyles.AllowInnerWhiteを指定すればよいのですが)

### 参考
https://dobon.net/vb/dotnet/string/datetimeparse.html
いつも迷うので[カスタム日時書式指定文字列](https://docs.microsoft.com/ja-jp/dotnet/standard/base-types/custom-date-and-time-format-strings)

## Enumerable.Repeatを使って初期化したジャグ配列の値が書き換わる問題
Enumerable.Repeatを使って初期化すると同じ参照値を示しているため別の要素の値まで置き換わってしまいます。

```csharp
            var items = Enumerable.Repeat<char[]>((Enumerable.Repeat<char>('-', 3).ToArray()), 3).ToArray();
            items[0][0] = '!';
            Console.WriteLine("items[1][0]:" + items[1][0]); // items[1][0]:!
```
### 解決策
LINQのSelectを使って初期化するとジャグ配列を正しく生成できます。

```csharp
            var items1 = Enumerable.Range(0, 3).Select(v => (new char[3]).Select(w => '-').ToArray()).ToArray();
            items1[0][0] = '!';
            Console.WriteLine("items[1][0]:" + items1[1][0]); // items[1][0]:-
```
### 参考
http://emkcsharp.hatenablog.com/entry/2013/Advent
http://shirakamisauto.hatenablog.com/entry/2016/03/04/143710
https://qiita.com/yosizo@github/items/1adcff1fc974cde5256a


## 文字列やEnumの比較に==とEqualsとどちらを使うべきか分からない問題
文字列の比較にEqualsを使っていたのですが==の方がよいと聞いたのでまとめてみました。

```c#
            var str = "XXX";
            var num = 1;

            // ==だとコンパイルエラーが起こるため型の違いを発見しやすい
            Console.WriteLine(str == num); // CS0019 演算子 '==' を 'int' と 'string' 型のオペランドに適用することはできません
            Console.WriteLine(str.Equals(num));

            // ==だとnullの等価が可能
            Console.WriteLine(null == null);
            Console.WriteLine(null.Equals(null)); // CS0023 演算子 '.' は '<null>' 型のオペランドに適用できません

```

System.String.Equalsの中身は[Reference Source](https://referencesource.microsoft.com/#mscorlib/system/string.cs,62d7ef71d323486a)から引用すると

```c#
        public override bool Equals(Object obj) {
            if (this == null)                        //this is necessary to guard against reverse-pinvokes and
                throw new NullReferenceException();  //other callers who do not use the callvirt instruction
 
            String str = obj as String;
            if (str == null)
                return false;
 
            if (Object.ReferenceEquals(this, obj))
                return true;
 
            if (this.Length != str.Length)
                return false;
 
            return EqualsHelper(this, str);
        }
```
これを見ると、Equalsの引数の型が異なってもいい感じにStringにキャストされてしまうためコンパイルエラーが起こらないことが分かる。
また、Equalsは型によって比較の挙動が変わり、System.Object.Equalsは参照の比較になる。
C#8でnull安全が導入されると聞きましたし、文字列の比較は==を使っていった方がいいかもしれませんね。

さらにEnumも==での比較が良いです。
型チェックが行われるため別のEnumで同じ名前が使われていても異なる型になるためコンパイルエラーになるためです。

### 参考
http://sonic64.com/2006-02-02.html
http://publisher-plus.sakura.ne.jp/wordpress/?p=751
http://etc9.hatenablog.com/entry/2013/10/03/092927
(2019/3/23追記)

## 所感
LINQの遅延効果はうっかり忘れがちなので気を付けたいです。
恐らくそのうち増えます。