tag:blogger.com,1999:blog-59465307047421309702024-03-06T16:20:07.273+08:00Jax 的工作紀錄除了在整理學習上的經驗,同時也能幫助其他需要的人Jax Huhttp://www.blogger.com/profile/01953021685585893658noreply@blogger.comBlogger4125tag:blogger.com,1999:blog-5946530704742130970.post-77170394557555981952021-10-16T23:09:00.001+08:002021-10-16T23:09:33.443+08:00用 Exception 作為使用者錯誤訊息的通道<p>自訂一個 UserException 來傳遞給使用者的訊息,然後程式裡只要簡單的 throw 訊息,接著就是在全域的錯誤處理中將訊息呈現給使用者。</p>
<p>這是一種非常方便的方式,因為可以無視 method 的階層深度,在程式的任何地方都可以將訊息傳到使用者面前。</p>
<p>原則上盡可能將錯誤轉化成 UserException,並且錯誤訊息要能讓使用者<code>[明確知道]</code>要如何正確操作軟體,但是只有工程師才可以修正的錯誤就應該記錄下來,並給使用者這樣的訊息<code>[系統出現錯誤,請通知管理者]</code>,只有盡可能的修正錯誤才不會讓使用者常常打電話吵你。</p>
Jax Huhttp://www.blogger.com/profile/01953021685585893658noreply@blogger.com0tag:blogger.com,1999:blog-5946530704742130970.post-75816354379789685932021-10-15T17:26:00.004+08:002023-02-25T18:17:08.463+08:00全域的 Exception 處理<p>大部分具有 Exception 機制的程式語言都有提供全域的 Exception 處理,如果你已經有用 log 去記錄錯誤的習慣的話,與其在程式裡佈滿了 try catch,不如在全域處裡中去記錄沒有被 catch 的 Exception,這樣程式就可以更乾淨了,而且程式如果發生異常的關閉時也會進入全域處裡,可以確保 Exception 妥善地被記錄。</p>
<br/>
<h3>Console, WinForm 程式</h3>
<pre class="cs" name="code">
using System;
using System.Security.Permissions;
using System.Threading;
using System.Windows.Forms;
using NLog;
namespace AppThreadException
{
static class Program
{
private static readonly ILogger _log = LogManager.GetCurrentClassLogger();
[STAThread]
[SecurityPermission(SecurityAction.Demand, Flags = SecurityPermissionFlag.ControlAppDomain)]
public static void Main(string[] args)
{
/* ThreadException 用來攔截 UI 錯誤 */
Application.ThreadException += threadExceptionHandler;
/* UnhandledException 只能攔截錯誤,不能阻止程式關閉 */
Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException);
AppDomain.CurrentDomain.UnhandledException += unhandledExceptionHandler;
Application.Run(new MyForm());
}
/// <summary>攔截 UI 錯誤</summary>
private static void threadExceptionHandler(object sender, ThreadExceptionEventArgs e)
{
_log.Fatal(e.Exception, "操作錯誤");
MessageBox.Show(e.Exception.Message, "操作錯誤", MessageBoxButtons.OK, MessageBoxIcon.Stop);
}
/// <summary>攔截不可挽回的錯誤,不能阻止程式關閉</summary>
private static void unhandledExceptionHandler(object sender, UnhandledExceptionEventArgs e)
{
Exception ex = (Exception)e.ExceptionObject;
_log.Fatal(ex, "執行錯誤");
MessageBox.Show(ex.Message, "執行錯誤", MessageBoxButtons.OK, MessageBoxIcon.Stop);
}
}
}
</pre>
<br/>
<h3>Web 程式</h3>
<p>在 Global.asax.cs 中可以設定 Application_Error 就可以攔截未處裡的 Exception,除了用這個方法記錄錯誤,還可以用現有的套件(<a href="https://elmah.github.io/a/mvc/" target="_blank">elmah</a>)幫我們完成。</p>
<pre class="cs" name="code">
using System;
using System.Web.Mvc;
using System.Web.Optimization;
using System.Web.Routing;
using NLog;
namespace MvcReport
{
public class MvcApplication : System.Web.HttpApplication
{
private static readonly ILogger _log = LogManager.GetCurrentClassLogger();
//...
protected void Application_Error(object sender, EventArgs e)
{
Exception ex = Server.GetLastError();
_log.Fatal(ex, ex.Message);
}
//...
}
}
</pre>
<br/>
<h3>Net core 程式</h3>
<p>Net core 的程式都是相同的方式,Web Request 的要另外配置</p>
<pre class="cs" name="code">
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Autofac.Extensions.DependencyInjection;
using EverTop.Api;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;
using NLog;
namespace MyWebApp
{
public class Program
{
private static readonly ILogger _log = LogManager.GetCurrentClassLogger();
public static void Main(string[] args)
{
/* UnhandledException 只能攔截錯誤,不能阻止程式關閉 */
AppDomain.CurrentDomain.UnhandledException += unhandledExceptionHandler;
/* 用來攔截 Task 錯誤 */
TaskScheduler.UnobservedTaskException += unobservedTaskException;
var host = Host.CreateDefaultBuilder(args)
.UseServiceProviderFactory(new AutofacServiceProviderFactory())
.ConfigureWebHostDefaults(webHostBuilder => webHostBuilder
.UseStartup<Startup>()
)
.Build();
host.Run(); /* 啟動網站 */
}
private static void unhandledExceptionHandler(object sender, UnhandledExceptionEventArgs e)
{
Exception ex = (Exception)e.ExceptionObject;
_log.Fatal(ex, "執行錯誤");
}
private static void unobservedTaskException(object sender, UnobservedTaskExceptionEventArgs e)
{
_log.Fatal(e.Exception, "執行錯誤");
e.SetObserved();
}
}
}
</pre>
<br/>
<h3>Net core 3.1 Web 程式</h3>
<p>在 Startup.cs 中配置 middleware 進行 Exception 攔截</p>
<pre class="cs" name="code">
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (_env.IsProduction())
{
app.UseExceptionHandler("/Error");
}
else
{
app.UseDeveloperExceptionPage();
}
app.UseStatusCodePagesWithReExecute("/Error");
/* 利用 middleware 進行 Exception 攔截 */
/* 這裡的順序很重要,不然會被前面 ExceptionHandler 處理掉就拿不到 Exception */
app.Use(async (context, next) =>
{
try
{
await next();
}
catch (Exception ex)
{
_log.Fatal(ex, "執行錯誤");
throw; /* 把 Exception 再丟出去給別人處理 */
}
});
}
</pre>
<br/>
<h3>週期性 Thread 的處裡方式</h3>
<p>將主要邏輯寫在另外 method 裡,這樣可以專注在 Exception 上。</p>
<pre class="cs" name="code">
private bool _runFlag = false;
public void Start()
{
if (_runFlag) { return; }
_runFlag = true;
var thread = new Thread(() =>
{
while (_runFlag)
{
try
{
cycleHandler();
}
catch (Exception ex)
{
_log.Fatal(ex, "執行錯誤");
}
Thread.Sleep(1000);
}
});
thread.Start();
}
public void Stop()
{
_runFlag = false;
}
private void cycleHandler()
{
// 主要的邏輯程式寫在這裡
}
</pre>
<br/>
<h3>PHP 錯誤處理</h3>
<p>Ref: <a href="https://www.php.net/manual/zh/function.set-error-handler.php" target="_blank">set_error_handler</a>, <a href="https://www.php.net/manual/zh/function.set-exception-handler.php" target="_blank">set_exception_handler</a></p>
<pre class="php" name="code">
<?php
function error_handler($errno, $errstr, $errfile, $errline) {
if(error_reporting() === 0){ return; }
throw new ErrorException($errstr, 0, $errno, $errfile, $errline);
}
set_error_handler('error_handler', E_ALL^E_NOTICE);
function exception_handler($exception) {
// 在這記錄 log
throw $exception;
}
set_exception_handler('exception_handler');
throw new Exception('Uncaught Exception');
//$a = 1 / 0;
</pre>
Jax Huhttp://www.blogger.com/profile/01953021685585893658noreply@blogger.com0tag:blogger.com,1999:blog-5946530704742130970.post-7058641818976980032021-10-14T17:21:00.002+08:002023-02-25T18:19:29.839+08:00Exception 是傳遞訊息的通道<pre class="cs:nogutter:nocontrols" name="code">
void show(string tag)
{
Console.WriteLine(tag);
}
void methodA()
{
throw new Exception("error msg");
show("A");
}
void methodB()
{
methodA();
show("B");
}
void methodC()
{
try
{
methodB();
show("C");
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
</pre>
<p>這裡為了方便我們理解將程式展開成下面的樣子:</p>
<pre class="cs:nogutter:nocontrols" name="code">
void show(string tag)
{
Console.WriteLine(tag);
}
void methodC()
{
try
{
methodB();
void methodB()
{
methodA();
void methodA()
{
throw new Exception("error msg");
//-----------------------------------------------------
show("A");
}
show("B");
}
show("C");
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
</pre>
<p>從範例中我們可以看出兩個特性:</p>
<br/>
<h3>* 通透性</h3>
<p>從 throw 的那一行開始 Exception 會一直向上傳遞,直到 catch 的區段。</p>
<br/>
<h3>* 脫離性</h3>
<p>從 throw 的那一行之後的程式都不會被執行,會有類似 return 的效果,不同的是會對每一層的 method 都進行脫離,包含 try 的區段也會脫離。</p>
<br/>
<p>methodC 的 catch 會捕獲從 methodA 丟出的 Exception,並且 Exception 中的 StackTrace 會紀錄 methodA, methodB, methodC,而 show() 都不會被執行。</p>
<p>由於大部分的程式邏輯都可以轉化成 else 就離開的程式流程,正好符合 throw 的脫離性,所以我們一開始在撰寫程式邏輯的時候,可以先專注在一般正常的流程邏輯上,之後再去補足檢查邏輯或脫離邏輯,最後在合適的位置進行 try catch 處理,或者在全域的錯誤處理中進行處理。</p>
<br/>
<p>這裡有一個糟糕的 method,他用了個 Result 物件來裝載回傳結果以及錯誤訊息:</p>
<pre class="cs:nogutter:nocontrols" name="code">
public class Result
{
public int Value { get; set; }
public string Error { get; set; }
}
public Result BadMethod(int input)
{
var result = new Result();
if (input > 0)
{
result.Value = 100 / input;
}
else
{
result.Error = "input 需要大於零";
}
return result;
}
</pre>
<p>如果用 Exception 可以讓程式簡單很多:</p>
<pre class="cs:nogutter:nocontrols" name="code">
public int GoodMethod(int input)
{
if (input <= 0)
{
throw new Exception("input 需要大於零");
}
return 100 / input;
}
</pre>Jax Huhttp://www.blogger.com/profile/01953021685585893658noreply@blogger.com0tag:blogger.com,1999:blog-5946530704742130970.post-34143836736278381222021-10-13T16:45:00.007+08:002023-02-25T18:20:10.075+08:00初學 Exception 的疑問<p>一開始學習 Exception 一定會有很多疑問,或者根本沒搞清楚就寫了一堆 try catch,讓程式邏輯變得亂七八糟難以閱讀,甚至小小的修改都不知道從何下手,Exception 是一種邏輯的輔助工具,可以讓你事半功倍,也可以變成噩夢。</p>
<p>我也是摸索了很久才對 Exception 有一定的應用認識,不敢說有多專精,畢竟每種語言跟特性都不同,但也是一定程度的應用經驗,我認為最重要的用途有兩個:</p>
<br/>
<h3>* 用 finally 去關閉資源物件</h3>
<p>資源物件有 File, Socket,...,這類的物件在邏輯結束後一定要進行關閉,強調一定一定,實在是吃了自己埋的雷了,沒有關閉的話資源會被咬住,像 Serial Port 這種底層資源沒有關閉的話,想要重新連接就只能重開機了,這是讓人無法接受的。</p>
<pre class="cs:nogutter:nocontrols" name="code">
FileStream file1 = null;
try
{
file1 = File.Open("<file path>", FileMode.OpenOrCreate);
// Do something
}
finally
{
if (file1 != null) { file1.Close(); }
}
/*== C# 可以用 using 代替上面的程式 ==*/
using (FileStream file2 = File.Open("<file path>", FileMode.OpenOrCreate))
{
// Do something
}
</pre>
<br/>
<br/>
<h3>* 用 catch 去記錄 Exception</h3>
<p>為了日後可以在日後除錯時留下線索,Exception 的記錄是很重要的,任何程式都有可能出現錯誤,不管是資料錯誤還是邏輯錯誤,有除錯的線索肯定會有很大的幫助,無論程式的大小都應該留下錯誤紀錄。</p>
<pre class="cs:nogutter:nocontrols" name="code">
try
{
// Do something
}
catch (Expression ex)
{
_log.Error(ex, "An error occurred.");
}
</pre>
<br/>
<p>其餘的清況,在不知道為何要用 try catch 時,最好就不要寫 try catch,基本上程式邏輯一旦出錯就只能退出,如果胡亂地進行修正讓邏輯繼續下去,可能會出現很糟糕的局面,而且資料錯誤就應該修正資料,邏輯錯誤就應該修正邏輯。</p>
<p>錯誤就是要讓人發現才有辦法修正,胡亂的將錯誤屏蔽掉只會埋下未來的苦果,也別用這種方式報復前東家,這只會搞到後面接手的倒楣鬼,本是同根生,相煎何太急。</p>
Jax Huhttp://www.blogger.com/profile/01953021685585893658noreply@blogger.com0