- 1 委託
- 1.1 簡介
- 1.2 操作使用
- 1.2.1 宣告委託(Delegate)
- 1.2.2 例項化委託(Delegate)
- 1.2.3 直接呼叫和invoke
- 1.2.4 Invoke 和 BeginInvoke
- 1.3 委託的多播
- 1.4 委託的匿名和lambda
- 1.4.1 匿名方法
- 1.4.2 lambda 表示式
- 1.5 內建委託
- 1.5.1 Action系列
- 1.5.2 Func 系列
- 1.5.3 Predicate
- 1.6 示例
- 2 事件
- 2.1 簡介
- 2.2 原理
- 2.2.1 講解
- 2.2.2 add 和 remove 訪問器
- 2.3 使用原生委託
- 2.4 自定義委託
- 2.4.1 宣告
- 2.4.2 操作
- 2.4.2.1 示例一
- 2.4.2.2 示例二
1 委託
1.1 簡介
C# 中的委託(Delegate
)類似於 C 或 C++ 中函式的指標。委託(Delegate
) 是存有對某個方法的引用的一種引用型別變數。引用可在執行時被改變。
委託(Delegate
)特別用於實現事件
和回撥
方法。所有的委託(Delegate
)都派生自 System.Delegate
類。
1.2 操作使用
1.2.1 宣告委託(Delegate)
委託宣告決定了可由該委託引用的方法。委託可指向一個與其具有相同標籤的方法。
例如,假設有一個委託:public delegate int MyDelegate (string s);
上面的委託可被用於引用任何一個帶有一個單一的 string 引數的方法,並返回一個 int 型別變數
宣告委託的語法如下:
delegate <return type> <delegate-name> <parameter list>
1.2.2 例項化委託(Delegate)
一旦宣告瞭委託型別,委託物件必須使用 new
關鍵字來建立,且與一個特定的方法有關。當建立委託時,傳遞到 new
語句的引數就像方法呼叫一樣書寫,但是不帶有引數
。例如:
public delegate void printString(string s);
...
printString ps1 = new printString(WriteToScreen);
printString ps2 = new printString(WriteToFile);
下面的例項演示了委託的宣告、例項化和使用,該委託可用於引用帶有一個整型引數的方法,並返回一個整型值。
using System;
delegate int NumberChanger(int n);
namespace DelegateAppl
{
class TestDelegate
{
static int num = 10;
public static int AddNum(int p)
{
num += p;
return num;
}
public static int MultNum(int q)
{
num *= q;
return num;
}
public static int getNum()
{
return num;
}
static void Main(string[] args)
{
// 建立委託例項
NumberChanger nc1 = new NumberChanger(AddNum);
NumberChanger nc2 = new NumberChanger(MultNum);
// 使用委託物件呼叫方法
nc1(25);
Console.WriteLine("Value of Num: {0}", getNum());
nc2(5);
Console.WriteLine("Value of Num: {0}", getNum());
Console.ReadKey();
}
}
}
結果:
Value of Num: 35
Value of Num: 175
注意
:呼叫委託對應方法一般是透過invoke
方法,但是從 C# 2.0
開始,委託的呼叫可以直接使用方法呼叫語法,而不需要顯式呼叫 Invoke
方法。
1.2.3 直接呼叫和invoke
雖然委託呼叫底層實際上是透過 Invoke
方法實現的,但語法上允許直接呼叫委託,就像呼叫普通方法一樣。換句話說,呼叫委託
和直接呼叫 Invoke
方法是等效的。
假設我們有一個 Action 型別的委託:
Action action = () => Console.WriteLine("Hello, Delegate!");
直接呼叫委託:
action(); // 輸出: Hello, Delegate!
透過 Invoke 方法呼叫:
action.Invoke(); // 輸出: Hello, Delegate!
兩種方式的結果完全一樣,因為 ()
是對委託物件 Invoke
方法的簡化語法糖
。
為什麼允許直接呼叫?
簡潔性
:如果每次呼叫都必須寫 .Invoke,程式碼顯得冗長。因此,C# 提供了直接呼叫語法,增強程式碼可讀性。語法糖
:編譯器在編譯時會自動將直接呼叫委託語法轉換為 Invoke 方法的呼叫。
即:action();
實際被編譯為:action.Invoke();
- 優先推薦直接呼叫
直接呼叫的方式更加簡潔可讀,因此在大多數情況下,推薦使用action()
而不是顯式呼叫action.Invoke()
。
為什麼保留 Invoke 方法?雖然直接呼叫語法更方便,但在某些特殊場景下,顯式呼叫 Invoke 方法可能更合適:
反射場景
:透過反射呼叫委託時,需要使用Invoke
方法。動態場景
:在動態生成程式碼或動態委託時,Invoke
方法更明確。
using System;
using System.Reflection;
class Program
{
static void Main()
{
Action action = PrintMessage;
// 使用反射呼叫 Invoke
MethodInfo invokeMethod = action.GetType().GetMethod("Invoke");
invokeMethod.Invoke(action, null);
}
static void PrintMessage()
{
Console.WriteLine("Hello, Reflection!");
}
}
輸出:
Hello, Reflection!
1.2.4 Invoke 和 BeginInvoke
委託的 Invoke
和 BeginInvoke
方法分別用於同步
和非同步
呼叫委託。它們的主要區別體現在呼叫方式、執行緒管理和返回結果的處理上。
Invoke 和 BeginInvoke 的區別
特性 | Invoke | BeginInvoke |
---|---|---|
呼叫型別 | 同步呼叫 | 非同步呼叫 |
執行緒阻塞 | 當前執行緒會阻塞,直到方法執行完成 | 當前執行緒不會阻塞 |
返回結果 | 直接返回方法的返回值 | 返回 IAsyncResult 物件,透過 EndInvoke 獲取返回值 |
異常處理 | 異常會直接在呼叫執行緒中丟擲 | 異常在呼叫 EndInvoke 時丟擲 |
執行緒使用 | 在呼叫執行緒上執行方法 | 線上程池中執行方法 |
使用場景 | 方法較快且呼叫執行緒不能被中斷時 | 方法較慢且需要非同步執行時 |
- Invoke:同步呼叫
定義:Invoke
是同步呼叫,當前執行緒會等待方法執行完畢後再繼續執行後續程式碼。
特點:- 阻塞呼叫:呼叫執行緒會被阻塞,直到被呼叫的方法完成。
- 返回結果:直接返回被呼叫方法的返回值(如果有)。
- 異常處理:如果被呼叫的方法丟擲異常,異常會在呼叫執行緒中傳播。
// 定義一個委託
delegate int AddDelegate(int x, int y);
AddDelegate add = (x, y) => x + y;
// 同步呼叫
int result = add.Invoke(3, 4);
Console.WriteLine($"Result: {result}"); // 輸出:Result: 7
- BeginInvoke:非同步呼叫
定義:BeginInvoke
是非同步呼叫,立即返回一個 IAsyncResult 物件,並不會阻塞呼叫執行緒。
特點:- 非阻塞呼叫:呼叫執行緒可以繼續執行其他程式碼,而被呼叫的方法在後臺執行緒中執行。
- 回撥機制:可以透過傳遞迴調方法或輪詢 IAsyncResult 物件來獲取結果。
需要顯式呼叫 EndInvoke 方法以獲取結果或處理異常。
// 定義一個委託
delegate int AddDelegate(int x, int y);
AddDelegate add = (x, y) =>
{
Console.WriteLine("Adding...");
System.Threading.Thread.Sleep(2000); // 模擬耗時操作
return x + y;
};
// 非同步呼叫
IAsyncResult asyncResult = add.BeginInvoke(3, 4, null, null);
// 主執行緒繼續執行其他任務
Console.WriteLine("Doing other work...");
// 獲取非同步呼叫結果
int result = add.EndInvoke(asyncResult);
Console.WriteLine($"Result: {result}"); // 輸出:Result: 7
BeginInvoke 的回撥,可以透過回撥函式在非同步操作完成後處理結果:
void CallbackMethod(IAsyncResult ar)
{
// 獲取委託例項
AddDelegate add = (AddDelegate)ar.AsyncState;
// 獲取結果
int result = add.EndInvoke(ar);
Console.WriteLine($"Result in Callback: {result}");
}
AddDelegate add = (x, y) =>
{
Console.WriteLine("Adding...");
System.Threading.Thread.Sleep(2000);
return x + y;
};
// 非同步呼叫並指定回撥函式
add.BeginInvoke(5, 7, CallbackMethod, add);
// 主執行緒繼續工作
Console.WriteLine("Doing other work...");
注意事項:
- BeginInvoke 使用執行緒池中的執行緒來執行方法,因此需要注意執行緒池的資源消耗。
必須呼叫 EndInvoke: - 呼叫 BeginInvoke 後,無論是否需要結果,都必須呼叫 EndInvoke,否則可能會導致資源洩漏。
- 推薦使用 Task 和 async/await:
- 在現代 C# 中,推薦使用 Task 和
async/await
替代BeginInvoke
和EndInvoke
,因為它們更易讀且不易出錯。
1.3 委託的多播
委託物件可使用 +
運算子進行合併。一個合併委託呼叫它所合併的兩個委託。只有相同型別的委託可被合併。-
運算子可用於從合併的委託中移除元件委託。
使用委託的這個有用的特點,可以建立一個委託被呼叫時要呼叫的方法的呼叫列表。這被稱為委託的 多播(multicasting)
,也叫組播
。
+
和 -
運算子確實可以直接用於委託物件的合併和移除,但這和 +=
和 -=
的用法有所不同。它們的區別主要在於運算場景
和賦值方式
。具體來說
+
和-
運算子:用於直接建立新的委託物件,不影響原始委託。它們不會修改原始委託,而是生成一個新的多播委託物件。- 使用
+
合併兩個委託物件,生成一個新的多播委託。 - 使用
-
從多播委託中移除一個委託,生成一個新的委託物件。
- 使用
+=
和-=
運算子:用於修改已有的委託例項,直接在原始變數上新增或移除委託。+=
將一個委託新增到現有委託鏈上,結果賦給原變數。-=
從現有委託鏈中移除一個委託,結果賦給原變數。
下面的程式演示了委託的多播:
using System;
delegate int NumberChanger(int n);
namespace DelegateAppl
{
class TestDelegate
{
static int num = 10;
public static int AddNum(int p)
{
num += p;
return num;
}
public static int MultNum(int q)
{
num *= q;
return num;
}
public static int getNum()
{
return num;
}
static void Main(string[] args)
{
// 建立委託例項
NumberChanger nc;
NumberChanger nc1 = new NumberChanger(AddNum);
NumberChanger nc2 = new NumberChanger(MultNum);
nc = nc1;
nc += nc2;
// 呼叫多播
nc(5);
Console.WriteLine("Value of Num: {0}", getNum());
Console.ReadKey();
}
}
}
結果:
Value of Num: 75
注意
:
在C#中,當使用+=
運算子向委託新增方法時,有兩種方式是等效的:
- 顯式地建立一個新的委託例項並將其新增到現有的委託鏈中
myDelegate += new NumberChanger(AddNum); - 省略new 部分,直接新增方法。
C#
編譯器會自動為您處理委託的例項化(如果必要的話):myDelegate += AddNum
這兩種方式在功能上是完全相同的。從C# 2.0開始,第二種方式(省略new關鍵字和委託型別)變得更加流行,因為它更簡潔,並且減少了不必要的程式碼。
1.4 委託的匿名和lambda
二者比較:
特性 | 匿名方法 (delegate) | Lambda 表示式 |
---|---|---|
語法簡潔性 | 較繁瑣,需要顯式寫出 delegate 關鍵字和引數列表 |
更簡潔,直接用 (引數) => {} 表達邏輯 |
表示式形式支援 | 不支援表示式形式,必須用 {} 包裹邏輯塊 | 支援表示式形式,單行邏輯可以省略 {} 和 return |
捕獲外部變數(閉包) | 支援 | 支援 |
語法風格 | 更接近傳統 C# 方法宣告 | 更現代、函數語言程式設計風格 |
語義清晰性 | delegate 明確表明它是匿名方法 |
使用 => 運算子,強調簡潔和函式式思想 |
1.4.1 匿名方法
匿名方法是透過使用 delegate
關鍵字建立委託例項來宣告的。
語法
delegate(parameters) { statement; }
例如:
delegate void NumberChanger(int n);
...
NumberChanger nc = delegate(int x)
{
Console.WriteLine("Anonymous Method: {0}", x);
};
程式碼塊 Console.WriteLine("Anonymous Method: {0}", x);
是匿名方法的主體。
委託可以透過匿名方法呼叫,也可以透過命名方法呼叫,即,透過向委託物件傳遞方法引數。
using System;
delegate void NumberChanger(int n);
namespace DelegateAppl
{
class TestDelegate
{
static int num = 10;
public static void AddNum(int p)
{
num += p;
Console.WriteLine("Named Method: {0}", num);
}
public static void MultNum(int q)
{
num *= q;
Console.WriteLine("Named Method: {0}", num);
}
static void Main(string[] args)
{
// 使用匿名方法建立委託例項
NumberChanger nc = delegate(int x)
{
Console.WriteLine("Anonymous Method: {0}", x);
};
// 使用匿名方法呼叫委託
nc(10);
// 使用命名方法例項化委託
nc = new NumberChanger(AddNum);
// 使用命名方法呼叫委託
nc(5);
// 使用另一個命名方法例項化委託
nc = new NumberChanger(MultNum);
// 使用命名方法呼叫委託
nc(2);
Console.ReadKey();
}
}
}
1.4.2 lambda 表示式
在 C# 2.0 及更高版本中,引入了 lambda 表示式,它是一種更簡潔的語法形式,用於編寫匿名方法。並且 從 C# 2.0
開始對委託的例項化做了簡化,委託型別的例項化在某些情況下可以省略顯式使用 new
關鍵字
使用 lambda 表示式:
using System;
delegate void NumberChanger(int n);
namespace DelegateAppl
{
class TestDelegate
{
static int num = 10;
public static void AddNum(int p)
{
num += p;
Console.WriteLine("Named Method: {0}", num);
}
public static void MultNum(int q)
{
num *= q;
Console.WriteLine("Named Method: {0}", num);
}
static void Main(string[] args)
{
// 使用 lambda 表示式建立委託例項
NumberChanger nc = x => Console.WriteLine($"Lambda Expression: {x}");
// 使用 lambda 表示式呼叫委託
nc(10);
// 使用命名方法例項化委託
nc = new NumberChanger(AddNum);
// 使用命名方法呼叫委託
nc(5);
// 使用另一個命名方法例項化委託
nc = new NumberChanger(MultNum);
// 使用命名方法呼叫委託
nc(2);
Console.ReadKey();
}
}
}
1.5 內建委託
C# 提供了一些內建的泛型委託,可以覆蓋大部分常見場景,主要包括以下幾個
1.5.1 Action系列
Action
是一個用於定義沒有返回值的方法的委託。支援最多 16 個引數的過載。
Action action = () => Console.WriteLine("No parameters");
action();
Action<int, string> actionWithParams = (x, y) => Console.WriteLine($"x: {x}, y: {y}");
actionWithParams(10, "hello");
1.5.2 Func 系列
Func
是一個帶有返回值的泛型委託。最多支援 16 個輸入引數,最後一個泛型引數是返回值的型別,前面的泛型參數列示輸入引數
Func<int, int, int> add = (x, y) => x + y;
int result = add(3, 5);
Console.WriteLine(result); // 輸出 8
1.5.3 Predicate
Predicate<T>
是一個返回 bool
的泛型委託,常用於過濾或條件判斷。
Predicate<int> isEven = x => x % 2 == 0;
bool check = isEven(4);
Console.WriteLine(check); // 輸出 True
1.6 示例
下面的例項演示了委託的用法。委託 printString 可用於引用帶有一個字串作為輸入的方法,並不返回任何東西。
我們使用這個委託來呼叫兩個方法,第一個把字串列印到控制檯,第二個把字串列印到檔案:
using System;
using System.IO;
namespace DelegateAppl
{
class PrintString
{
static FileStream fs;
static StreamWriter sw;
// 委託宣告
public delegate void printString(string s);
// 該方法列印到控制檯
public static void WriteToScreen(string str)
{
Console.WriteLine("The String is: {0}", str);
}
// 該方法列印到檔案
public static void WriteToFile(string s)
{
fs = new FileStream("c:\\message.txt", FileMode.Append, FileAccess.Write);
sw = new StreamWriter(fs);
sw.WriteLine(s);
sw.Flush();
sw.Close();
fs.Close();
}
// 該方法把委託作為引數,並使用它呼叫方法
public static void sendString(printString ps)
{
ps("Hello World");
}
static void Main(string[] args)
{
printString ps1 = new printString(WriteToScreen);
printString ps2 = new printString(WriteToFile);
sendString(ps1);
sendString(ps2);
Console.ReadKey();
}
}
}
結果:
The String is: Hello World
2 事件
2.1 簡介
C# 事件(Event
)是一種成員,用於將特定的事件通知傳送給訂閱者。事件通常用於實現觀察者模式
,它允許一個物件將狀態的變化通知其他物件,而不需要知道這些物件的細節。
事件(Event
) 基本上說是一個使用者操作,如按鍵、點選、滑鼠移動等等,或者是一些提示資訊,如系統生成的通知。應用程式需要在事件發生時響應事件。例如,中斷。
C# 中使用事件機制實現執行緒間的通訊。
關鍵點:
- 宣告委託:定義事件將使用的委託型別。委託是一個函式簽名。
- 宣告事件:使用
event
關鍵字宣告一個事件。 - 觸發事件:在適當的時候呼叫事件,通知所有訂閱者。
- 訂閱和取消訂閱事件:其他類可以透過
+=
和-=
運算子訂閱和取消訂閱事件。
事件模型五個組成部分:
- 事件的擁有者
- 事件成員
- 事件的響應者
- 事件處理器
- 事件訂閱--把事件處理器與事件關聯在一起,本質是一種以委託型別為基礎的
2.2 原理
2.2.1 講解
事件在類中宣告且生成,且透過使用同一個類或其他類中的委託
與事件
處理程式關聯。包含事件的類用於釋出事件。這被稱為 釋出器(publisher
) 類。其他接受該事件的類被稱為 訂閱器(subscriber
) 類。事件使用 釋出-訂閱(publisher-subscriber
) 模型。
- 釋出器(
publisher
) 是一個包含事件和委託定義的物件。事件和委託之間的聯絡也定義在這個物件中。釋出器類的物件呼叫這個事件,並通知其他的物件。 - 訂閱器(
subscriber
) 是一個接受事件並提供事件處理程式的物件。在釋出器類中的委託呼叫訂閱器(subscriber
)類中的方法(事件處理程式)
在C#中,通常使用+=
運算子來訂閱事件,使用-=
運算子來取消訂閱事件
2.2.2 add 和 remove 訪問器
自己定義事件的 add
和 remove
訪問器,從而控制事件訂閱和取消訂閱的具體行為。
下面的 EventHandler 是系統自帶 事件,不用宣告
public class EventDemo
{
private EventHandler _myEvent;
// 自定義事件
public event EventHandler MyEvent
{
add
{
Console.WriteLine("Adding a subscriber");
_myEvent += value;
}
remove
{
Console.WriteLine("Removing a subscriber");
_myEvent -= value;
}
}
public void TriggerEvent()
{
_myEvent?.Invoke(this, EventArgs.Empty);
}
}
class Program
{
static void Main()
{
EventDemo demo = new EventDemo();
EventHandler handler = (sender, e) => Console.WriteLine("Event triggered!");
// 訂閱事件
demo.MyEvent += handler; // 輸出: Adding a subscriber
// 觸發事件
demo.TriggerEvent(); // 輸出: Event triggered!
// 取消訂閱事件
demo.MyEvent -= handler; // 輸出: Removing a subscriber
}
}
自定義 add 和 remove 訪問器通常在以下場景中使用:
- 自定義訂閱邏輯:需要記錄訂閱者或對訂閱者進行篩選時。
- 執行緒安全:確保事件的訂閱和取消訂閱在多執行緒環境下安全。
- 限制訂閱數量:控制最多隻能有特定數量的訂閱者。
- 日誌記錄或除錯:每次事件訂閱或取消時記錄相關資訊。
注意事項:
- 事件是委託的包裝:事件是基於委託的,但它對委託的直接訪問進行了限制,提供了一種更安全的機制來管理委託呼叫。
- 事件預設行為:如果不需要特殊邏輯,直接使用預設的 add 和 remove,即可滿足大部分場景。
- 不要直接對事件賦值:只能透過
+=
和-=
訪問事件。直接賦值(如 MyEvent = null)是不允許的,除非是在宣告類內部。
2.3 使用原生委託
namespace EventExample
{
class Program
{
MyForm form = new MyForm();
// 寫此處原生對應的 事件可以先寫此處名字,讓visualstudio 自動生成對應引數型別的 事件
form.Click += form.FormClicked;
form.ShowDialog();
}
class MyForm : Form
{
internal void FormClicked(object sender,EventArgs e)
{
this.Text = DataTime.Now.ToString();
}
}
}
2.4 自定義委託
2.4.1 宣告
在類的內部宣告事件,首先必須宣告該事件的委託型別。
例如:
public delegate void BoilerLogHandler(string status);
然後,宣告事件本身,使用 event
關鍵字:
// 基於上面的委託定義事件
public event BoilerLogHandler BoilerEventLog;
上面的程式碼定義了一個名為 BoilerLogHandler 的委託和一個名為 BoilerEventLog 的事件,該事件在生成的時候會呼叫委託。
2.4.2 操作
2.4.2.1 示例一
以下示例展示瞭如何在 C# 中使用事件:
using System;
namespace EventDemo
{
// 定義一個委託型別,用於事件處理程式
public delegate void NotifyEventHandler(object sender, EventArgs e);
// 釋出者類
public class ProcessBusinessLogic
{
// 宣告事件
public event NotifyEventHandler ProcessCompleted;
// 觸發事件的方法
protected virtual void OnProcessCompleted(EventArgs e)
{
ProcessCompleted?.Invoke(this, e);
}
// 模擬業務邏輯過程並觸發事件
public void StartProcess()
{
Console.WriteLine("Process Started!");
// 這裡可以加入實際的業務邏輯
// 業務邏輯完成,觸發事件
OnProcessCompleted(EventArgs.Empty);
}
}
// 訂閱者類
public class EventSubscriber
{
public void Subscribe(ProcessBusinessLogic process)
{
process.ProcessCompleted += Process_ProcessCompleted;
}
private void Process_ProcessCompleted(object sender, EventArgs e)
{
Console.WriteLine("Process Completed!");
}
}
class Program
{
static void Main(string[] args)
{
ProcessBusinessLogic process = new ProcessBusinessLogic();
EventSubscriber subscriber = new EventSubscriber();
// 訂閱事件
subscriber.Subscribe(process);
// 啟動過程
process.StartProcess();
Console.ReadLine();
}
}
}
說明
- 定義委託型別:
public delegate void NotifyEventHandler(object sender, EventArgs e);
這是一個委託型別,它定義了事件處理程式的簽名。通常使用EventHandler
或EventHandler<TEventArgs>
來替代自定義的委託。 - 宣告事件:
public event NotifyEventHandler ProcessCompleted;
這是一個使用 NotifyEventHandler 委託型別的事件。 - 觸發事件:
protected virtual void OnProcessCompleted(EventArgs e)
{
ProcessCompleted?.Invoke(this, e);
}
這是一個受保護的方法,用於觸發事件。使用 ?.Invoke
語法來確保只有在有訂閱者時才呼叫事件。
- 訂閱和取消訂閱事件:
process.ProcessCompleted += Process_ProcessCompleted;
訂閱者使用+=
運算子訂閱事件,並定義事件處理程式 Process_ProcessCompleted。
2.4.2.2 示例二
using System;
namespace SimpleEvent
{
using System;
/***********釋出器類***********/
public class EventTest
{
private int value;
public delegate void NumManipulationHandler();
public event NumManipulationHandler ChangeNum;
protected virtual void OnNumChanged()
{
if ( ChangeNum != null )
{
ChangeNum(); /* 事件被觸發 */
}else {
Console.WriteLine( "event not fire" );
Console.ReadKey(); /* 回車繼續 */
}
}
public EventTest()
{
int n = 5;
SetValue( n );
}
public void SetValue( int n )
{
if ( value != n )
{
value = n;
OnNumChanged();
}
}
}
/***********訂閱器類***********/
public class subscribEvent
{
public void printf()
{
Console.WriteLine( "event fire" );
Console.ReadKey(); /* 回車繼續 */
}
}
/***********觸發***********/
public class MainClass
{
public static void Main()
{
EventTest e = new EventTest(); /* 例項化物件,第一次沒有觸發事件 */
subscribEvent v = new subscribEvent(); /* 例項化物件 */
e.ChangeNum += new EventTest.NumManipulationHandler( v.printf ); /* 註冊 */
e.SetValue( 7 );
e.SetValue( 11 );
}
}
}
結果:
event not fire
event fire
event fire