C#: Python中try/catch/else块的等效方法

9
在Python中,有这个有用的异常处理代码:
try:
    # Code that could raise an exception
except Exception:
    # Exception handling
else:
    # Code to execute if the try block DID NOT fail

我认为将有可能引发异常的代码与正常代码分开是非常有用的。在Python中,这是可能的,如上所示,但我在C#中找不到类似的功能。

假设不存在该功能或类似功能,那么将正常代码放在try块内还是在catch块后面是标准做法吗?

我之所以问这个问题是因为我有以下代码:

if (!IsReadOnly)
{
    T newobj;
    try
    {
        newobj = DataPortal.Update<T>(this);

        List<string> keys = new List<string>(BasicProperties.Keys);
        foreach (string key in keys)
        {
            BasicProperties[key] = newobj.BasicProperties[key];
        }
    }
    catch (DataPortalException)
    {
        // TODO: Implement DataPortal.Update<T>() recovery mechanism
    }
}

需要将常规代码放在try块中,否则如果引发异常并随后处理,则newobj将未分配,但这使得将此类大量代码放入与DataPortalException无关的try块中感觉非常不自然。该怎么办呢?
谢谢

1
我一直讨厌使用try catch块,因为这个原因。没有办法区分已经中断的代码和未中断的代码。 - Nick Bedford
1
投票关闭为精确重复:https://dev59.com/RXM_5IYBdhLWcg3w43pt - ChristopheD
这可能是一个重复的问题,但那个问题从未得到充分的回答。阅读了答案后,我最喜欢Daniel Newby的概念。 - Noctis Skytower
10个回答

9
我希望看到try/catch之外的代码,这样就清楚了你要捕获的异常来自哪里,也不会意外地捕获你不想捕获的异常。
我认为最接近Python的try/catch/else的等效方法是使用本地布尔变量来记住是否抛出了异常。
bool success;

try
{
    foo();
    success = true;
}
catch (MyException)
{
    recover();
    success = false;
}

if (success)
{
    bar();
}

但是如果您这样做,我会问为什么您不完全恢复异常,以便可以继续仿佛已经成功,或者全面中止通过返回错误代码甚至让异常传播给调用者。


1
在“适当处理异常”部分加1分。如果您无法从异常中恢复,那么您应该立即停止正在进行的操作。如果您无法优雅地停止,请重构代码以实现优雅停止。 - kibibu
1
+1,但我必须问:这种方式有什么比直接在catch前调用bar()更好的地方吗? - 3Dave
1
@David:根据我提供的代码,如果bar函数抛出了MyException异常,则不会被捕获,但是根据您的建议,它将被捕获。我展示的方法更接近于try: catch: else:结构,因为在else中引发的异常不会被捕获。 - Mark Byers
1
我经常使用这个模式,通常将success初始化为false - 这样可以在catch块中节省一行代码。 - Aaronaught
1
有时候,使用 pre_do; try { do } catch (error) { recover } else { do_more }; post_do; 是正确的。这并不意味着错误没有被恢复或代码没有完全中止它需要中止的部分。 - Noctis Skytower

8

简陋的解决方案:创建一个继承自Exception的Else类,在try块的结尾抛出它的实例,并使用catch (Else) {...}来处理其他内容。

我感觉很不舒服。


4
我不知道该怎么做 - 这个情况必须投反对票,但我觉得很好笑,因此犹豫了一下。 - Sam Harwell
2
在Python中,else不能捕获所有其他异常。只有在没有异常时才会执行else语句块。 - Nate

3

C#没有这样的概念,所以你只剩下三个选择:

  • 将else代码放在try内部。
  • 将else代码放在try catch块外部,使用本地变量来表示成功或失败,并在else代码周围使用if块。
  • 将else代码放在finally块中,使用本地变量来表示成功或失败,并在else代码周围使用if块。

3
这可能会被踩,但C#语言不是有goto语句吗?(请注意,我对C#的了解非常有限,所以我不知道这是否可行。)
还有像这样的内容:
try 
{ 
...
} 
catch(Exception ex) 
{ 
...
goto Jump_past_tryelse
} 
...//Code to execute if the try block DID NOT fail

Jump_past_tryelse:
...

我差点因为你提供了整个酒吧的“goto”而给你一个+1! - amelvin
4
“offering out the whole pub” 翻译为中文是“出售整个酒吧”。 - Roman A. Taycher
@RomanA.Taycher 八年后,谷歌搜索“offering out the whole pub”只有一个结果:这个页面。我想我们永远不会知道了…… - MarredCheese

2

将你的“else”块放在catch之前。这样,只有当代码执行到该点时才会执行:

try
{
    fee();
    fi();
    foe();
    fum();

    /// put your "else" stuff here. 
    /// It will only be executed if fee-fi-foe-fum did not fail.
}
catch(Exception e)
{
    // handle exception
}

考虑到这一点,除非原始问题描述中缺少关键信息,否则我看不出try..catch...else的用处。

5
区别在于有1行代码可能会抛出异常,你预料到了这种情况并进行了捕获处理。如果其他不应该出现异常的代码行抛出了异常,那么你就不希望捕获它,因为这可能意味着真实的错误(bug)。 - Davy8
@Davy8,如果是这样的话,你应该要么只捕获你期望的特定异常类型,要么将你不希望生成异常的代码放在这个try..catch块之外。无论哪种情况,你仍然应该捕获任何发生的异常,以便你可以优雅地失败。 - 3Dave
1
它很接近,但不完全等同。如果它只是在 try/catch 的外面,它将会执行,无论第一部分是否抛出异常,但它应该只在第一部分没有抛出异常时执行。有可能抛出相同类型的异常,所以仅捕获特定异常是一种改进,但并不完全相同。 - Davy8
@DavidLively:你不应该从一个本不应存在的错误中优雅地失败。如果需要保持某种一致性,调用代码应该确保并报告错误。 - Noctis Skytower
与“应该存在”的错误相反?我同意使用异常来控制流程是不好的。例如,如果您正在发布需要隐藏真正异常的库,则将日志记录留给调用者可能是不可接受的。有些情况 - 比如与外部设备通信 - 是无法通过设计解决这种问题的。 - 3Dave
如果你喜欢先行动再征得许可,那么异常并不一定意味着程序有漏洞。我为我的措辞道歉。某些失败或异常可能是可以接受的,而其他一些则明显是程序中的错误或漏洞。Daniel Newby的答案可能不是首选,但我喜欢它从Python翻译到C#的效果。 - Noctis Skytower

2

请允许我重复一个来自类似的StackOverflow问题的想法。你无法直接这样做,但是可以编写一个封装所需行为的方法。如果您不熟悉lambda表达式和Func委托,请查看原始问题以了解如何实现该方法。使用方式可能是这样的:

TryExceptRaise(() => { 
    // code that can throw exception
  }, (Exception e) => { 
    // code to run in case of an exception
    return (...); 
  }, () => {
    // code to run if there is no exception
    return (...);
  });

1

使用C# 7 版本,可以使用局部函数来模拟此行为:

示例1:(自C# 7版本以来)

void Main()
{
    void checkedCode()
    {
        try 
        {
            foo();
        }
        catch (Exception ex)
        {
            recover();
            return;
        }
        // ElseCode here
    }
    checkedCode();
}

如果你喜欢使用lambda语法,你也可以声明一个run方法。
void Run(Action r) { r(); }

只需要在代码中出现一次,然后使用匿名方法的模式如下

示例2:(较旧版本的C#和C# 7版本)

Run(() => {
    try
    {
        foo();
    }
    catch (Exception)
    {
        recover();
        return;
    }
    // ElseCode here
});

无论何时需要在安全上下文中封装代码。


DotNetFiddle中尝试一下吧

using System;

public class Program
{
    public static void Main()
    {
        var objDemo = new Demo();

        objDemo.runWithException = false;
        objDemo.Example1();

        // now the same with exception
        objDemo.runWithException = true;
        objDemo.Example1();

        Console.WriteLine();

        objDemo.runWithException = false;
        objDemo.Example2();

        // now the same with exception
        objDemo.runWithException = true;
        objDemo.Example2();

    }

}

public class Demo
{

    public bool runWithException { get; set; } = false;

    public void Example1()
    {
        void checkedCode()
        {
            try
            {
                foo();
            }
            catch (Exception)
            {
                recover();
                return;
            }
            // ElseCode here
            Console.WriteLine("Hello from else");
        }
        checkedCode();
    }

    void Run(Action r) { r(); }
    public void Example2()
    {
        Run(() =>
        {
            try
            {
                foo();
            }
            catch (Exception)
            {
                recover();
                return;
            }
            // ElseCode here
            Console.WriteLine("Hello from else");
        });
    }

    public void foo()
    {
       Console.WriteLine("Hello from foo()");
       if (this.runWithException) throw new ApplicationException("TestException");
    }


    public void recover()
    {
        Console.WriteLine("Hello from recover()");
    }

}

注意:

  • 在这两个示例中,创建了一个函数上下文,以便我们可以使用return;在出现错误时退出。
  • 你可以在JavaScript中找到类似于示例2中使用的模式:自调用匿名函数(例如JQuery使用它们)。因为在C#中不能自调用,所以使用了辅助方法Run
  • 由于Run不必是一个局部函数,所以示例2也适用于旧版本的C#。

0
你可以像这样做:
if (!IsReadOnly)
{
    T newobj = null;
    try
    {
        newobj = DataPortal.Update<T>(this);    
    }
    catch (DataPortalException)
    {
        // TODO: Implement DataPortal.Update<T>() recovery mechanism
    }
    if (newobj != null)
    {
        List<string> keys = new List<string>(BasicProperties.Keys);
        foreach (string key in keys)
        {
            BasicProperties[key] = newobj.BasicProperties[key];
        }
    }
}

0

那将是像空语句一样的命中

try 
{ 
    somethingThatCanThrow(); 
} 
catch(Exception ex) 
{ 
    LogException(ex); 
    return;
} 
ContinueFlow();

0
if (!IsReadOnly)
        {
            T newobj;
            bool Done;
            try
            {
                newobj = DataPortal.Update<T>(this);
                List<string> keys = new List<string>(BasicProperties.Keys);
                foreach (string key in keys)
                {
                    BasicProperties[key] = newobj.BasicProperties[key];
                }
                Done = true;
            }
            catch (DataPortalException)
            {
                // TODO: Implement DataPortal.Update<T>() recovery mechanism
                Done = false;
            }
            finally
            {
                if (newobj != null && Done == false)
                {
                    List<string> keys = new List<string>(BasicProperties.Keys);
                    foreach (string key in keys)
                    {
                        BasicProperties[key] = newobj.BasicProperties[key];
                    }
                }
            }
        }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接