关于异常处理:C#try-catch-else

C# try-catch-else

从Python到C#的异常处理困扰着我的一件事是,在C#中似乎没有任何指定else子句的方法。例如,在Python中,我可以编写这样的内容(请注意,这只是一个示例。我不是在问什么是读取文件的最佳方法):

1
2
3
4
5
6
7
8
9
10
11
12
13
try
{
    reader = new StreamReader(path);
}
catch (Exception)
{
    // Uh oh something went wrong with opening the file for reading
}
else
{
    string line = reader.ReadLine();
    char character = line[30];
}

根据我在大多数C#代码中看到的内容,人们只会编写以下内容:

1
2
3
4
5
6
7
8
9
10
try
{
    reader = new StreamReader(path);
    string line = reader.ReadLine();
    char character = line[30];
}
catch (Exception)
{
    // Uh oh something went wrong, but where?
}

此问题是我不想超出范围异常,原因是文件的第一行可能包含不超过30个字符。我只想捕获与读取文件流有关的异常。我可以在C#中使用任何类似的构造来实现相同的功能吗?


捕获特定类别的异常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
try
{
    reader = new StreamReader(path);
    string line = reader.ReadLine();
    char character = line[30];
}
catch (IOException ex)
{
    // Uh oh something went wrong with I/O
}
catch (Exception ex)
{
    // Uh oh something else went wrong
    throw; // unless you're very sure what you're doing here.
}

第二个捕获当然是可选的。而且由于您不知道发生了什么,因此吞下这个最一般的异常非常危险。


您可以这样写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
bool success = false;
try {
    reader = new StreamReader(path);
    success = true;
}
catch(Exception) {
    // Uh oh something went wrong with opening the file for reading
}
finally {
    if(success) {
        string line = reader.ReadLine();    
        char character = line[30];
    }
}


捕获更具体的异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
try {
   reader = new StreamReader(path);
   string line = reader.ReadLine();
   char character = line[30];
}
catch(FileNotFoundException e) {
   // thrown by StreamReader constructor
}
catch(DirectoryNotFoundException e) {
   // thrown by StreamReader constructor
}
catch(IOException e) {
   // some other fatal IO error occured
}

通常,进一步处理尽可能特殊的异常,并避免处理基本的System.Exception


您可以执行以下操作:

1
2
3
4
5
6
7
8
9
10
11
try
{
    reader = new StreamReader(path);
}
catch (Exception)
{
    // Uh oh something went wrong with opening the file for reading
}

string line = reader.ReadLine();
char character = line[30];

但是,当然,您必须将reader设置为正确的状态,或者将return设置为方法之外的内容。


您也可以嵌套try语句


.NET中对异常的用法有所不同;它们仅适用于特殊情况。

实际上,除非您知道异常的含义并且可以对此采取实际措施,否则您不应捕获异常。


Is there any similar construct I can use in C#
to acheive the same thing?

不。

用" if "语句package索引访问器,这是在性能和??可读性方面最好的解决方案。

1
2
3
if (line.length > 30) {
   char character = line [30];
}

习惯上来说,您将使用using语句将文件打开操作与对其包含的数据所做的工作分开(并在退出时包括自动清除)

1
2
3
4
5
6
7
8
9
10
try {
  using (reader = new StreamReader(path))
  {
    DoSomethingWith(reader);
  }
}
catch(IOException ex)
{
  // Log ex here
}

最好也避免捕获所有可能的异常-就像那些告诉您运行时即将到期的异常一样。


您可以有多个catch子句,每个子句都特定于您希望捕获的异常类型。因此,如果只想捕获IOExceptions,则可以将catch子句更改为:

1
2
3
4
5
6
7
8
9
try
{
    reader = new StreamReader(path);
    string line = reader.ReadLine();
    char character = line[30];
}
catch (IOException)
{    
}

除IOException外,其他任何东西都将在调用堆栈中传播。如果您还想处理其他异常,则可以添加多个异常子句,但是必须确保以最特定于大多数通用顺序的方式添加它们。例如:

1
2
3
4
5
6
7
8
9
10
11
12
try
{
    reader = new StreamReader(path);
    string line = reader.ReadLine();
    char character = line[30];
}
catch (IOException)
{    
}
catch (Exception)
{
}


在C#中可能没有对try { ... } catch { ... } else { ... }的任何本机支持,但是如果您愿意承担使用替代方法的开销,那么下面显示的示例可能会很有吸引力:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
using System;

public class Test
{
    public static void Main()
    {
        Example("ksEE5A.exe");
    }

    public static char Example(string path) {
        var reader = default(System.IO.StreamReader);
        var line = default(string);
        var character = default(char);
        TryElse(
            delegate {
                Console.WriteLine("Trying to open StreamReader ...");
                reader = new System.IO.StreamReader(path);
            },
            delegate {
                Console.WriteLine("Success!");
                line = reader.ReadLine();
                character = line[30];
            },
            null,
            new Case(typeof(NullReferenceException), error => {
                Console.WriteLine("Something was null and should not have been.");
                Console.WriteLine("The line variable could not cause this error.");
            }),
            new Case(typeof(System.IO.FileNotFoundException), error => {
                Console.WriteLine("File could not be found:");
                Console.WriteLine(path);
            }),
            new Case(typeof(Exception), error => {
                Console.WriteLine("There was an error:");
                Console.WriteLine(error);
            }));
        return character;
    }

    public static void TryElse(Action pyTry, Action pyElse, Action pyFinally, params Case[] pyExcept) {
        if (pyElse != null && pyExcept.Length < 1) {
            throw new ArgumentException(@"there must be exception handlers if else is specified", nameof(pyExcept));
        }
        var doElse = false;
        var savedError = default(Exception);
        try {
            try {
                pyTry();
                doElse = true;
            } catch (Exception error) {
                savedError = error;
                foreach (var handler in pyExcept) {
                    if (handler.IsMatch(error)) {
                        handler.Process(error);
                        savedError = null;
                        break;
                    }
                }
            }
            if (doElse) {
                pyElse();
            }
        } catch (Exception error) {
            savedError = error;
        }
        pyFinally?.Invoke();
        if (savedError != null) {
            throw savedError;
        }
    }
}

public class Case {
    private Type ExceptionType { get; }
    public Action<Exception> Process { get; }
    private Func<Exception, bool> When { get; }

    public Case(Type exceptionType, Action<Exception> handler, Func<Exception, bool> when = null) {
        if (!typeof(Exception).IsAssignableFrom(exceptionType)) {
            throw new ArgumentException(@"exceptionType must be a type of exception", nameof(exceptionType));
        }
        this.ExceptionType = exceptionType;
        this.Process = handler;
        this.When = when;
    }

    public bool IsMatch(Exception error) {
        return this.ExceptionType.IsInstanceOfType(error) && (this.When?.Invoke(error) ?? true);
    }
}

听起来像您只想在第一件事成功的情况下才做第二件事。也许捕获不同类别的异常是不合适的,例如,如果两个语句都可以抛出相同类别的异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
try
{
    reader1 = new StreamReader(path1);
    // if we got this far, path 1 succeded, so try path2
    try
    {
        reader2 = new StreamReader(path2);

    }
    catch (OIException ex)
    {
        // Uh oh something went wrong with opening the file2 for reading
        // Nevertheless, have a look at file1. Its fine!
    }
}
catch (OIException ex)
{
    // Uh oh something went wrong with opening the file1 for reading.
    // So I didn't even try to open file2
}

看到其他建议的解决方案后,这是我的方法:

1
2
3
4
5
6
7
8
9
10
11
try {
    reader = new StreamReader(path);
}
catch(Exception ex) {
    // Uh oh something went wrong with opening the file stream
    MyOpeningFileStreamException newEx = new MyOpeningFileStreamException();
    newEx.InnerException = ex;
    throw(newEx);
}
    string line = reader.ReadLine();
    char character = line[30];

当然,只有在您对通过打开文件流(在此处为示例)引发的任何异常感兴趣的情况下,这样做才有意义,并且与应用程序中的所有其他异常分开。在应用程序的更高级别,然后您可以按照自己的意愿处理MyOpeningFileStreamException

由于未检查的异常,您永远不能百分百确定仅从整个代码块中捕获IOException就足够了-StreamReader可以决定现在还是在内部抛出其他类型的异常未来。


我已经自由地对您的代码进行了一些改动,以说明一些要点。

using构造用于打开文件。如果引发了异常,则即使您没有捕获到异常,也必须记住关闭文件。可以使用try { } catch () { } finally { }构造完成此操作,但是using指令对此要好得多。它保证当using块的作用域结束时,将丢弃在内部创建的变量。对于文件,意味着它将被关闭。

通过研究StreamReader构造函数和ReadLine方法的文档,您可以看到可能会引发哪些异常。然后,您可以找到自己认为合适的人。请注意,记录的例外列表并不总是完整的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// May throw FileNotFoundException, DirectoryNotFoundException,
// IOException and more.
try {
  using (StreamReader streamReader = new StreamReader(path)) {
    try {
      String line;
      // May throw IOException.
      while ((line = streamReader.ReadLine()) != null) {
        // May throw IndexOutOfRangeException.
        Char c = line[30];
        Console.WriteLine(c);
      }
    }
    catch (IOException ex) {
      Console.WriteLine("Error reading file:" + ex.Message);
    }
  }
}
catch (FileNotFoundException ex) {
  Console.WriteLine("File does not exists:" + ex.Message);
}
catch (DirectoryNotFoundException ex) {
  Console.WriteLine("Invalid path:" + ex.Message);
}
catch (IOException ex) {
  Console.WriteLine("Error reading file:" + ex.Message);
}


如果碰巧处于循环中,则可以将catch语句放入catch块中。这将导致跳过该块的其余代码。

如果您不在循环中,则无需在此级别捕获异常。让它沿调用堆栈向上传播到知道如何处理的catch块。您可以通过在当前级别消除整个try / catch框架来做到这一点。

我也喜欢在Python中使用try / except / else,也许有一天它们会被添加到C#中(就像有多个返回值一样)。但是,如果您对异常的看法有所不同,则其他块并不是严格必需的。