[{"data":1,"prerenderedAt":1218},["ShallowReactive",2],{"blog-\u002Fblog\u002Fdotnet-delegates-events-observer-pattern":3},{"id":4,"title":5,"body":6,"description":1203,"difficulty":1204,"extension":1205,"framework":1206,"frameworkSlug":1207,"meta":1208,"navigation":59,"order":50,"path":1209,"qaPath":1210,"seo":1211,"stem":1212,"subtopic":1213,"topic":1214,"topicSlug":1215,"updated":1216,"__hash__":1217},"blog\u002Fblog\u002Fdotnet-delegates-events-observer-pattern.md","C# Delegates, Events, and the Observer Pattern",{"type":7,"value":8,"toc":1188},"minimark",[9,14,18,22,30,119,122,126,217,231,235,238,337,344,369,373,388,493,496,504,519,661,668,672,679,749,759,763,766,861,866,945,952,956,967,1025,1059,1067,1074,1142,1148,1152,1184],[10,11,13],"h2",{"id":12},"why-delegates-and-events-come-up-in-every-c-interview","Why delegates and events come up in every C# interview",[15,16,17],"p",{},"Delegates are the foundation of C#'s callback model. Events are the pub-sub mechanism\nbuilt on top of them. Understanding both — and the pitfalls around memory leaks, closures,\nand multicast invocation — separates candidates who use these features from candidates\nwho truly understand them.",[10,19,21],{"id":20},"what-a-delegate-is","What a delegate is",[15,23,24,25,29],{},"A ",[26,27,28],"strong",{},"delegate"," is a type-safe function pointer. Unlike C function pointers, a delegate is\nan object — it can be stored, passed as a parameter, returned from a method, and combined\nwith other delegates.",[31,32,37],"pre",{"className":33,"code":34,"language":35,"meta":36,"style":36},"language-csharp shiki shiki-themes github-light github-dark","\u002F\u002F Declare a delegate type — defines the method signature:\ndelegate string Transform(string input);\n\n\u002F\u002F Assign any method that matches the signature:\nTransform upper = s => s.ToUpper();\nTransform lower = s => s.ToLower();\n\nConsole.WriteLine(upper(\"hello\")); \u002F\u002F HELLO\nConsole.WriteLine(lower(\"WORLD\")); \u002F\u002F world\n\n\u002F\u002F Pass as a parameter:\nstring ApplyTwice(Transform t, string s) => t(t(s));\nConsole.WriteLine(ApplyTwice(upper, \"test\")); \u002F\u002F TEST\n","csharp","",[38,39,40,48,54,61,67,73,79,84,90,96,101,107,113],"code",{"__ignoreMap":36},[41,42,45],"span",{"class":43,"line":44},"line",1,[41,46,47],{},"\u002F\u002F Declare a delegate type — defines the method signature:\n",[41,49,51],{"class":43,"line":50},2,[41,52,53],{},"delegate string Transform(string input);\n",[41,55,57],{"class":43,"line":56},3,[41,58,60],{"emptyLinePlaceholder":59},true,"\n",[41,62,64],{"class":43,"line":63},4,[41,65,66],{},"\u002F\u002F Assign any method that matches the signature:\n",[41,68,70],{"class":43,"line":69},5,[41,71,72],{},"Transform upper = s => s.ToUpper();\n",[41,74,76],{"class":43,"line":75},6,[41,77,78],{},"Transform lower = s => s.ToLower();\n",[41,80,82],{"class":43,"line":81},7,[41,83,60],{"emptyLinePlaceholder":59},[41,85,87],{"class":43,"line":86},8,[41,88,89],{},"Console.WriteLine(upper(\"hello\")); \u002F\u002F HELLO\n",[41,91,93],{"class":43,"line":92},9,[41,94,95],{},"Console.WriteLine(lower(\"WORLD\")); \u002F\u002F world\n",[41,97,99],{"class":43,"line":98},10,[41,100,60],{"emptyLinePlaceholder":59},[41,102,104],{"class":43,"line":103},11,[41,105,106],{},"\u002F\u002F Pass as a parameter:\n",[41,108,110],{"class":43,"line":109},12,[41,111,112],{},"string ApplyTwice(Transform t, string s) => t(t(s));\n",[41,114,116],{"class":43,"line":115},13,[41,117,118],{},"Console.WriteLine(ApplyTwice(upper, \"test\")); \u002F\u002F TEST\n",[15,120,121],{},"In practice, you almost never declare custom delegate types because the built-in generics\ncover every case.",[10,123,125],{"id":124},"action-func-and-predicate","Action, Func, and Predicate",[31,127,129],{"className":33,"code":128,"language":35,"meta":36,"style":36},"\u002F\u002F Action\u003CT1..T16> — void return:\nAction                 log    = () => Console.WriteLine(\"event\");\nAction\u003Cstring>         print  = msg => Console.WriteLine(msg);\nAction\u003Cstring, int>    repeat = (msg, n) => Enumerable.Range(0, n).ToList().ForEach(_ => Console.WriteLine(msg));\n\n\u002F\u002F Func\u003CT1..T16, TResult> — non-void return:\nFunc\u003Cint, int>         square = n => n * n;\nFunc\u003Cstring, int>      len    = s => s.Length;\nFunc\u003Cint, int, double> divide = (a, b) => (double)a \u002F b;\n\nConsole.WriteLine(square(5));     \u002F\u002F 25\nConsole.WriteLine(divide(7, 2)); \u002F\u002F 3.5\n\n\u002F\u002F Predicate\u003CT> — Func\u003CT, bool> with a different name:\nPredicate\u003Cstring> nonEmpty = s => s.Length > 0;\nvar names = new List\u003Cstring> { \"\", \"Alice\", \"\", \"Bob\" };\nvar valid = names.FindAll(nonEmpty); \u002F\u002F [\"Alice\", \"Bob\"]\n",[38,130,131,136,141,146,151,155,160,165,170,175,179,184,189,193,199,205,211],{"__ignoreMap":36},[41,132,133],{"class":43,"line":44},[41,134,135],{},"\u002F\u002F Action\u003CT1..T16> — void return:\n",[41,137,138],{"class":43,"line":50},[41,139,140],{},"Action                 log    = () => Console.WriteLine(\"event\");\n",[41,142,143],{"class":43,"line":56},[41,144,145],{},"Action\u003Cstring>         print  = msg => Console.WriteLine(msg);\n",[41,147,148],{"class":43,"line":63},[41,149,150],{},"Action\u003Cstring, int>    repeat = (msg, n) => Enumerable.Range(0, n).ToList().ForEach(_ => Console.WriteLine(msg));\n",[41,152,153],{"class":43,"line":69},[41,154,60],{"emptyLinePlaceholder":59},[41,156,157],{"class":43,"line":75},[41,158,159],{},"\u002F\u002F Func\u003CT1..T16, TResult> — non-void return:\n",[41,161,162],{"class":43,"line":81},[41,163,164],{},"Func\u003Cint, int>         square = n => n * n;\n",[41,166,167],{"class":43,"line":86},[41,168,169],{},"Func\u003Cstring, int>      len    = s => s.Length;\n",[41,171,172],{"class":43,"line":92},[41,173,174],{},"Func\u003Cint, int, double> divide = (a, b) => (double)a \u002F b;\n",[41,176,177],{"class":43,"line":98},[41,178,60],{"emptyLinePlaceholder":59},[41,180,181],{"class":43,"line":103},[41,182,183],{},"Console.WriteLine(square(5));     \u002F\u002F 25\n",[41,185,186],{"class":43,"line":109},[41,187,188],{},"Console.WriteLine(divide(7, 2)); \u002F\u002F 3.5\n",[41,190,191],{"class":43,"line":115},[41,192,60],{"emptyLinePlaceholder":59},[41,194,196],{"class":43,"line":195},14,[41,197,198],{},"\u002F\u002F Predicate\u003CT> — Func\u003CT, bool> with a different name:\n",[41,200,202],{"class":43,"line":201},15,[41,203,204],{},"Predicate\u003Cstring> nonEmpty = s => s.Length > 0;\n",[41,206,208],{"class":43,"line":207},16,[41,209,210],{},"var names = new List\u003Cstring> { \"\", \"Alice\", \"\", \"Bob\" };\n",[41,212,214],{"class":43,"line":213},17,[41,215,216],{},"var valid = names.FindAll(nonEmpty); \u002F\u002F [\"Alice\", \"Bob\"]\n",[15,218,219,222,223,226,227,230],{},[26,220,221],{},"Rule:"," Always prefer ",[38,224,225],{},"Action","\u002F",[38,228,229],{},"Func"," over custom delegate types. Custom delegates\nare only useful when you need XML-doc support for a specific callback or when a legacy\nAPI requires a named type.",[10,232,234],{"id":233},"multicast-delegates","Multicast delegates",[15,236,237],{},"All C# delegates are multicast — they can hold references to multiple methods:",[31,239,241],{"className":33,"code":240,"language":35,"meta":36,"style":36},"Action\u003Cstring> log    = msg => Console.WriteLine($\"[LOG] {msg}\");\nAction\u003Cstring> audit  = msg => Console.WriteLine($\"[AUDIT] {msg}\");\nAction\u003Cstring> metrics = msg => Console.WriteLine($\"[METRICS] {msg}\");\n\n\u002F\u002F Combine:\nAction\u003Cstring> all = log + audit + metrics;\nall(\"User logged in\");\n\u002F\u002F [LOG] User logged in\n\u002F\u002F [AUDIT] User logged in\n\u002F\u002F [METRICS] User logged in\n\n\u002F\u002F += adds a handler:\nAction\u003Cstring> chain = log;\nchain += audit;\nchain(\"Order placed\");\n\n\u002F\u002F -= removes a specific handler:\nchain -= audit;\nchain(\"Payment processed\"); \u002F\u002F only log runs\n",[38,242,243,248,253,258,262,267,272,277,282,287,292,296,301,306,311,316,320,325,331],{"__ignoreMap":36},[41,244,245],{"class":43,"line":44},[41,246,247],{},"Action\u003Cstring> log    = msg => Console.WriteLine($\"[LOG] {msg}\");\n",[41,249,250],{"class":43,"line":50},[41,251,252],{},"Action\u003Cstring> audit  = msg => Console.WriteLine($\"[AUDIT] {msg}\");\n",[41,254,255],{"class":43,"line":56},[41,256,257],{},"Action\u003Cstring> metrics = msg => Console.WriteLine($\"[METRICS] {msg}\");\n",[41,259,260],{"class":43,"line":63},[41,261,60],{"emptyLinePlaceholder":59},[41,263,264],{"class":43,"line":69},[41,265,266],{},"\u002F\u002F Combine:\n",[41,268,269],{"class":43,"line":75},[41,270,271],{},"Action\u003Cstring> all = log + audit + metrics;\n",[41,273,274],{"class":43,"line":81},[41,275,276],{},"all(\"User logged in\");\n",[41,278,279],{"class":43,"line":86},[41,280,281],{},"\u002F\u002F [LOG] User logged in\n",[41,283,284],{"class":43,"line":92},[41,285,286],{},"\u002F\u002F [AUDIT] User logged in\n",[41,288,289],{"class":43,"line":98},[41,290,291],{},"\u002F\u002F [METRICS] User logged in\n",[41,293,294],{"class":43,"line":103},[41,295,60],{"emptyLinePlaceholder":59},[41,297,298],{"class":43,"line":109},[41,299,300],{},"\u002F\u002F += adds a handler:\n",[41,302,303],{"class":43,"line":115},[41,304,305],{},"Action\u003Cstring> chain = log;\n",[41,307,308],{"class":43,"line":195},[41,309,310],{},"chain += audit;\n",[41,312,313],{"class":43,"line":201},[41,314,315],{},"chain(\"Order placed\");\n",[41,317,318],{"class":43,"line":207},[41,319,60],{"emptyLinePlaceholder":59},[41,321,322],{"class":43,"line":213},[41,323,324],{},"\u002F\u002F -= removes a specific handler:\n",[41,326,328],{"class":43,"line":327},18,[41,329,330],{},"chain -= audit;\n",[41,332,334],{"class":43,"line":333},19,[41,335,336],{},"chain(\"Payment processed\"); \u002F\u002F only log runs\n",[15,338,339,340,343],{},"When a multicast delegate has a return type, only the ",[26,341,342],{},"last"," invocation's return value\nis captured — earlier ones are discarded. If any handler throws, subsequent handlers in\nthe invocation list do not run.",[31,345,347],{"className":33,"code":346,"language":35,"meta":36,"style":36},"Func\u003Cint> counter = () => 1;\ncounter += () => 2;\ncounter += () => 3;\nConsole.WriteLine(counter()); \u002F\u002F 3 — only last return value\n",[38,348,349,354,359,364],{"__ignoreMap":36},[41,350,351],{"class":43,"line":44},[41,352,353],{},"Func\u003Cint> counter = () => 1;\n",[41,355,356],{"class":43,"line":50},[41,357,358],{},"counter += () => 2;\n",[41,360,361],{"class":43,"line":56},[41,362,363],{},"counter += () => 3;\n",[41,365,366],{"class":43,"line":63},[41,367,368],{},"Console.WriteLine(counter()); \u002F\u002F 3 — only last return value\n",[10,370,372],{"id":371},"events-encapsulated-multicast-delegates","Events — encapsulated multicast delegates",[15,374,375,376,379,380,383,384,387],{},"An ",[38,377,378],{},"event"," is a delegate with restricted access. Outside the declaring class, you can only\n",[38,381,382],{},"+="," or ",[38,385,386],{},"-=","; you cannot invoke or overwrite the delegate directly:",[31,389,391],{"className":33,"code":390,"language":35,"meta":36,"style":36},"public class Button\n{\n    \u002F\u002F Public delegate field — anyone can invoke or assign:\n    public Action\u003Cstring>? Clicked;\n\n    \u002F\u002F Event — only Button can invoke; subscribers can only += or -=:\n    public event Action\u003Cstring>? ClickedEvent;\n\n    public void Click(string label)\n    {\n        Clicked?.Invoke(label);\n        ClickedEvent?.Invoke(label);\n    }\n}\n\nvar btn = new Button();\nbtn.Clicked    = msg => Console.WriteLine(msg); \u002F\u002F overwrites existing handlers!\nbtn.ClickedEvent += msg => Console.WriteLine(msg); \u002F\u002F adds to invocation list\n\n\u002F\u002F btn.ClickedEvent(\"test\"); \u002F\u002F compile error outside Button — good!\n",[38,392,393,398,403,408,413,417,422,427,431,436,441,446,451,456,461,465,470,475,480,484],{"__ignoreMap":36},[41,394,395],{"class":43,"line":44},[41,396,397],{},"public class Button\n",[41,399,400],{"class":43,"line":50},[41,401,402],{},"{\n",[41,404,405],{"class":43,"line":56},[41,406,407],{},"    \u002F\u002F Public delegate field — anyone can invoke or assign:\n",[41,409,410],{"class":43,"line":63},[41,411,412],{},"    public Action\u003Cstring>? Clicked;\n",[41,414,415],{"class":43,"line":69},[41,416,60],{"emptyLinePlaceholder":59},[41,418,419],{"class":43,"line":75},[41,420,421],{},"    \u002F\u002F Event — only Button can invoke; subscribers can only += or -=:\n",[41,423,424],{"class":43,"line":81},[41,425,426],{},"    public event Action\u003Cstring>? ClickedEvent;\n",[41,428,429],{"class":43,"line":86},[41,430,60],{"emptyLinePlaceholder":59},[41,432,433],{"class":43,"line":92},[41,434,435],{},"    public void Click(string label)\n",[41,437,438],{"class":43,"line":98},[41,439,440],{},"    {\n",[41,442,443],{"class":43,"line":103},[41,444,445],{},"        Clicked?.Invoke(label);\n",[41,447,448],{"class":43,"line":109},[41,449,450],{},"        ClickedEvent?.Invoke(label);\n",[41,452,453],{"class":43,"line":115},[41,454,455],{},"    }\n",[41,457,458],{"class":43,"line":195},[41,459,460],{},"}\n",[41,462,463],{"class":43,"line":201},[41,464,60],{"emptyLinePlaceholder":59},[41,466,467],{"class":43,"line":207},[41,468,469],{},"var btn = new Button();\n",[41,471,472],{"class":43,"line":213},[41,473,474],{},"btn.Clicked    = msg => Console.WriteLine(msg); \u002F\u002F overwrites existing handlers!\n",[41,476,477],{"class":43,"line":327},[41,478,479],{},"btn.ClickedEvent += msg => Console.WriteLine(msg); \u002F\u002F adds to invocation list\n",[41,481,482],{"class":43,"line":333},[41,483,60],{"emptyLinePlaceholder":59},[41,485,487,490],{"class":43,"line":486},20,[41,488,489],{},"\u002F\u002F btn.ClickedEvent(\"test\");",[41,491,492],{}," \u002F\u002F compile error outside Button — good!\n",[15,494,495],{},"This encapsulation prevents one subscriber from accidentally replacing another's handler.",[10,497,499,500],{"id":498},"the-standard-eventhandler-pattern","The standard EventHandler",[501,502,503],"t-event-args",{}," pattern",[15,505,506,507,510,511,514,515,518],{},".NET's convention for events pairs ",[38,508,509],{},"EventHandler\u003CTEventArgs>"," with a custom ",[38,512,513],{},"EventArgs","\nsubclass and a ",[38,516,517],{},"protected virtual OnXxx"," raise method:",[31,520,522],{"className":33,"code":521,"language":35,"meta":36,"style":36},"public class StockEventArgs : EventArgs\n{\n    public string Symbol { get; }\n    public decimal Price { get; }\n    public StockEventArgs(string symbol, decimal price)\n        => (Symbol, Price) = (symbol, price);\n}\n\npublic class StockFeed\n{\n    public event EventHandler\u003CStockEventArgs>? PriceChanged;\n\n    protected virtual void OnPriceChanged(StockEventArgs e)\n        => PriceChanged?.Invoke(this, e); \u002F\u002F thread-safe null check + invoke\n\n    public void UpdatePrice(string symbol, decimal price)\n    {\n        \u002F\u002F ... internal logic ...\n        OnPriceChanged(new StockEventArgs(symbol, price));\n    }\n}\n\n\u002F\u002F Subscriber:\nvar feed = new StockFeed();\nfeed.PriceChanged += (sender, e) =>\n    Console.WriteLine($\"{e.Symbol}: {e.Price:C}\");\n\nfeed.UpdatePrice(\"MSFT\", 420.00m); \u002F\u002F MSFT: £420.00\n",[38,523,524,529,533,538,543,548,553,557,561,566,570,575,579,584,589,593,598,602,607,612,616,621,626,632,638,644,650,655],{"__ignoreMap":36},[41,525,526],{"class":43,"line":44},[41,527,528],{},"public class StockEventArgs : EventArgs\n",[41,530,531],{"class":43,"line":50},[41,532,402],{},[41,534,535],{"class":43,"line":56},[41,536,537],{},"    public string Symbol { get; }\n",[41,539,540],{"class":43,"line":63},[41,541,542],{},"    public decimal Price { get; }\n",[41,544,545],{"class":43,"line":69},[41,546,547],{},"    public StockEventArgs(string symbol, decimal price)\n",[41,549,550],{"class":43,"line":75},[41,551,552],{},"        => (Symbol, Price) = (symbol, price);\n",[41,554,555],{"class":43,"line":81},[41,556,460],{},[41,558,559],{"class":43,"line":86},[41,560,60],{"emptyLinePlaceholder":59},[41,562,563],{"class":43,"line":92},[41,564,565],{},"public class StockFeed\n",[41,567,568],{"class":43,"line":98},[41,569,402],{},[41,571,572],{"class":43,"line":103},[41,573,574],{},"    public event EventHandler\u003CStockEventArgs>? PriceChanged;\n",[41,576,577],{"class":43,"line":109},[41,578,60],{"emptyLinePlaceholder":59},[41,580,581],{"class":43,"line":115},[41,582,583],{},"    protected virtual void OnPriceChanged(StockEventArgs e)\n",[41,585,586],{"class":43,"line":195},[41,587,588],{},"        => PriceChanged?.Invoke(this, e); \u002F\u002F thread-safe null check + invoke\n",[41,590,591],{"class":43,"line":201},[41,592,60],{"emptyLinePlaceholder":59},[41,594,595],{"class":43,"line":207},[41,596,597],{},"    public void UpdatePrice(string symbol, decimal price)\n",[41,599,600],{"class":43,"line":213},[41,601,440],{},[41,603,604],{"class":43,"line":327},[41,605,606],{},"        \u002F\u002F ... internal logic ...\n",[41,608,609],{"class":43,"line":333},[41,610,611],{},"        OnPriceChanged(new StockEventArgs(symbol, price));\n",[41,613,614],{"class":43,"line":486},[41,615,455],{},[41,617,619],{"class":43,"line":618},21,[41,620,460],{},[41,622,624],{"class":43,"line":623},22,[41,625,60],{"emptyLinePlaceholder":59},[41,627,629],{"class":43,"line":628},23,[41,630,631],{},"\u002F\u002F Subscriber:\n",[41,633,635],{"class":43,"line":634},24,[41,636,637],{},"var feed = new StockFeed();\n",[41,639,641],{"class":43,"line":640},25,[41,642,643],{},"feed.PriceChanged += (sender, e) =>\n",[41,645,647],{"class":43,"line":646},26,[41,648,649],{},"    Console.WriteLine($\"{e.Symbol}: {e.Price:C}\");\n",[41,651,653],{"class":43,"line":652},27,[41,654,60],{"emptyLinePlaceholder":59},[41,656,658],{"class":43,"line":657},28,[41,659,660],{},"feed.UpdatePrice(\"MSFT\", 420.00m); \u002F\u002F MSFT: £420.00\n",[15,662,663,664,667],{},"The ",[38,665,666],{},"protected virtual"," raise method lets subclasses override the raise behavior without\nduplicating subscription logic.",[10,669,671],{"id":670},"closures-and-the-loop-capture-bug","Closures and the loop-capture bug",[15,673,674,675,678],{},"Lambdas capture the ",[26,676,677],{},"variable",", not the value at the time of capture:",[31,680,682],{"className":33,"code":681,"language":35,"meta":36,"style":36},"var actions = new List\u003CAction>();\nfor (int i = 0; i \u003C 3; i++)\n    actions.Add(() => Console.WriteLine(i)); \u002F\u002F captures the variable 'i'\n\nforeach (var a in actions) a(); \u002F\u002F prints: 3, 3, 3 — loop finished, i == 3!\n\n\u002F\u002F Fix: copy to a new local per iteration:\nvar fixed = new List\u003CAction>();\nfor (int i = 0; i \u003C 3; i++)\n{\n    int copy = i;                     \u002F\u002F new variable each iteration\n    fixed.Add(() => Console.WriteLine(copy));\n}\nforeach (var a in fixed) a(); \u002F\u002F 0, 1, 2 — correct!\n",[38,683,684,689,694,699,703,708,712,717,722,726,730,735,740,744],{"__ignoreMap":36},[41,685,686],{"class":43,"line":44},[41,687,688],{},"var actions = new List\u003CAction>();\n",[41,690,691],{"class":43,"line":50},[41,692,693],{},"for (int i = 0; i \u003C 3; i++)\n",[41,695,696],{"class":43,"line":56},[41,697,698],{},"    actions.Add(() => Console.WriteLine(i)); \u002F\u002F captures the variable 'i'\n",[41,700,701],{"class":43,"line":63},[41,702,60],{"emptyLinePlaceholder":59},[41,704,705],{"class":43,"line":69},[41,706,707],{},"foreach (var a in actions) a(); \u002F\u002F prints: 3, 3, 3 — loop finished, i == 3!\n",[41,709,710],{"class":43,"line":75},[41,711,60],{"emptyLinePlaceholder":59},[41,713,714],{"class":43,"line":81},[41,715,716],{},"\u002F\u002F Fix: copy to a new local per iteration:\n",[41,718,719],{"class":43,"line":86},[41,720,721],{},"var fixed = new List\u003CAction>();\n",[41,723,724],{"class":43,"line":92},[41,725,693],{},[41,727,728],{"class":43,"line":98},[41,729,402],{},[41,731,732],{"class":43,"line":103},[41,733,734],{},"    int copy = i;                     \u002F\u002F new variable each iteration\n",[41,736,737],{"class":43,"line":109},[41,738,739],{},"    fixed.Add(() => Console.WriteLine(copy));\n",[41,741,742],{"class":43,"line":115},[41,743,460],{},[41,745,746],{"class":43,"line":195},[41,747,748],{},"foreach (var a in fixed) a(); \u002F\u002F 0, 1, 2 — correct!\n",[15,750,663,751,754,755,758],{},[38,752,753],{},"foreach"," version avoids this in C# 5+: each iteration implicitly creates a new\nrange variable. The ",[38,756,757],{},"for"," loop does not.",[10,760,762],{"id":761},"the-event-memory-leak","The event memory leak",[15,764,765],{},"The most common source of memory leaks in .NET applications:",[31,767,769],{"className":33,"code":768,"language":35,"meta":36,"style":36},"public class EventSource\n{\n    public event EventHandler? DataChanged;\n}\n\npublic class Subscriber\n{\n    public Subscriber(EventSource source)\n    {\n        source.DataChanged += OnDataChanged; \u002F\u002F source holds reference to 'this'\n    }\n    private void OnDataChanged(object? sender, EventArgs e) { }\n}\n\nvar source = new EventSource(); \u002F\u002F long-lived singleton\n{\n    var sub = new Subscriber(source);\n} \u002F\u002F 'sub' out of scope — but source.DataChanged still references it!\n\nGC.Collect(); \u002F\u002F sub is NOT collected — source keeps it alive!\n",[38,770,771,776,780,785,789,793,798,802,807,811,816,820,825,829,833,838,842,847,852,856],{"__ignoreMap":36},[41,772,773],{"class":43,"line":44},[41,774,775],{},"public class EventSource\n",[41,777,778],{"class":43,"line":50},[41,779,402],{},[41,781,782],{"class":43,"line":56},[41,783,784],{},"    public event EventHandler? DataChanged;\n",[41,786,787],{"class":43,"line":63},[41,788,460],{},[41,790,791],{"class":43,"line":69},[41,792,60],{"emptyLinePlaceholder":59},[41,794,795],{"class":43,"line":75},[41,796,797],{},"public class Subscriber\n",[41,799,800],{"class":43,"line":81},[41,801,402],{},[41,803,804],{"class":43,"line":86},[41,805,806],{},"    public Subscriber(EventSource source)\n",[41,808,809],{"class":43,"line":92},[41,810,440],{},[41,812,813],{"class":43,"line":98},[41,814,815],{},"        source.DataChanged += OnDataChanged; \u002F\u002F source holds reference to 'this'\n",[41,817,818],{"class":43,"line":103},[41,819,455],{},[41,821,822],{"class":43,"line":109},[41,823,824],{},"    private void OnDataChanged(object? sender, EventArgs e) { }\n",[41,826,827],{"class":43,"line":115},[41,828,460],{},[41,830,831],{"class":43,"line":195},[41,832,60],{"emptyLinePlaceholder":59},[41,834,835],{"class":43,"line":201},[41,836,837],{},"var source = new EventSource(); \u002F\u002F long-lived singleton\n",[41,839,840],{"class":43,"line":207},[41,841,402],{},[41,843,844],{"class":43,"line":213},[41,845,846],{},"    var sub = new Subscriber(source);\n",[41,848,849],{"class":43,"line":327},[41,850,851],{},"} \u002F\u002F 'sub' out of scope — but source.DataChanged still references it!\n",[41,853,854],{"class":43,"line":333},[41,855,60],{"emptyLinePlaceholder":59},[41,857,858],{"class":43,"line":486},[41,859,860],{},"GC.Collect(); \u002F\u002F sub is NOT collected — source keeps it alive!\n",[15,862,863],{},[26,864,865],{},"Fix: always unsubscribe in Dispose:",[31,867,869],{"className":33,"code":868,"language":35,"meta":36,"style":36},"public class Subscriber : IDisposable\n{\n    private readonly EventSource _source;\n\n    public Subscriber(EventSource source)\n    {\n        _source = source;\n        _source.DataChanged += OnDataChanged;\n    }\n\n    private void OnDataChanged(object? sender, EventArgs e) { }\n\n    public void Dispose()\n    {\n        _source.DataChanged -= OnDataChanged; \u002F\u002F unsubscribe — reference released\n    }\n}\n",[38,870,871,876,880,885,889,893,897,902,907,911,915,919,923,928,932,937,941],{"__ignoreMap":36},[41,872,873],{"class":43,"line":44},[41,874,875],{},"public class Subscriber : IDisposable\n",[41,877,878],{"class":43,"line":50},[41,879,402],{},[41,881,882],{"class":43,"line":56},[41,883,884],{},"    private readonly EventSource _source;\n",[41,886,887],{"class":43,"line":63},[41,888,60],{"emptyLinePlaceholder":59},[41,890,891],{"class":43,"line":69},[41,892,806],{},[41,894,895],{"class":43,"line":75},[41,896,440],{},[41,898,899],{"class":43,"line":81},[41,900,901],{},"        _source = source;\n",[41,903,904],{"class":43,"line":86},[41,905,906],{},"        _source.DataChanged += OnDataChanged;\n",[41,908,909],{"class":43,"line":92},[41,910,455],{},[41,912,913],{"class":43,"line":98},[41,914,60],{"emptyLinePlaceholder":59},[41,916,917],{"class":43,"line":103},[41,918,824],{},[41,920,921],{"class":43,"line":109},[41,922,60],{"emptyLinePlaceholder":59},[41,924,925],{"class":43,"line":115},[41,926,927],{},"    public void Dispose()\n",[41,929,930],{"class":43,"line":195},[41,931,440],{},[41,933,934],{"class":43,"line":201},[41,935,936],{},"        _source.DataChanged -= OnDataChanged; \u002F\u002F unsubscribe — reference released\n",[41,938,939],{"class":43,"line":207},[41,940,455],{},[41,942,943],{"class":43,"line":213},[41,944,460],{},[15,946,947,948,951],{},"Always implement ",[38,949,950],{},"IDisposable"," and unsubscribe from events when the subscriber has a\nshorter intended lifetime than the publisher.",[10,953,955],{"id":954},"covariance-and-contravariance","Covariance and contravariance",[15,957,958,959,962,963,966],{},"Delegates support ",[26,960,961],{},"covariance on return types"," (more derived) and ",[26,964,965],{},"contravariance on\nparameter types"," (more general):",[31,968,970],{"className":33,"code":969,"language":35,"meta":36,"style":36},"class Animal { }\nclass Dog : Animal { }\n\n\u002F\u002F Covariance — Func\u003CDog> assignable to Func\u003CAnimal>:\nFunc\u003CDog>    getDog    = () => new Dog();\nFunc\u003CAnimal> getAnimal = getDog; \u002F\u002F Dog IS-A Animal\n\n\u002F\u002F Contravariance — Action\u003CAnimal> assignable to Action\u003CDog>:\nAction\u003CAnimal> feedAnimal = a => Console.WriteLine($\"Feeding {a.GetType().Name}\");\nAction\u003CDog>    feedDog    = feedAnimal; \u002F\u002F any Animal handler can handle a Dog\nfeedDog(new Dog());\n",[38,971,972,977,982,986,991,996,1001,1005,1010,1015,1020],{"__ignoreMap":36},[41,973,974],{"class":43,"line":44},[41,975,976],{},"class Animal { }\n",[41,978,979],{"class":43,"line":50},[41,980,981],{},"class Dog : Animal { }\n",[41,983,984],{"class":43,"line":56},[41,985,60],{"emptyLinePlaceholder":59},[41,987,988],{"class":43,"line":63},[41,989,990],{},"\u002F\u002F Covariance — Func\u003CDog> assignable to Func\u003CAnimal>:\n",[41,992,993],{"class":43,"line":69},[41,994,995],{},"Func\u003CDog>    getDog    = () => new Dog();\n",[41,997,998],{"class":43,"line":75},[41,999,1000],{},"Func\u003CAnimal> getAnimal = getDog; \u002F\u002F Dog IS-A Animal\n",[41,1002,1003],{"class":43,"line":81},[41,1004,60],{"emptyLinePlaceholder":59},[41,1006,1007],{"class":43,"line":86},[41,1008,1009],{},"\u002F\u002F Contravariance — Action\u003CAnimal> assignable to Action\u003CDog>:\n",[41,1011,1012],{"class":43,"line":92},[41,1013,1014],{},"Action\u003CAnimal> feedAnimal = a => Console.WriteLine($\"Feeding {a.GetType().Name}\");\n",[41,1016,1017],{"class":43,"line":98},[41,1018,1019],{},"Action\u003CDog>    feedDog    = feedAnimal; \u002F\u002F any Animal handler can handle a Dog\n",[41,1021,1022],{"class":43,"line":103},[41,1023,1024],{},"feedDog(new Dog());\n",[15,1026,1027,1029,1030,1033,1034,1037,1038,1041,1042,1045,1046,1029,1048,1050,1051,1054,1055,1058],{},[38,1028,229],{}," is ",[38,1031,1032],{},"out","-covariant on ",[38,1035,1036],{},"TResult","; ",[38,1039,1040],{},"in","-contravariant on each ",[38,1043,1044],{},"T"," parameter.\n",[38,1047,225],{},[38,1049,1040],{},"-contravariant. This is why ",[38,1052,1053],{},"Func\u003CDog>"," is a valid ",[38,1056,1057],{},"Func\u003CAnimal>"," —\nit satisfies the Liskov Substitution Principle at the delegate level.",[10,1060,1062,1063],{"id":1061},"iobservable-when-events-arent-enough","IObservable",[1064,1065,1066],"t",{}," — when events aren't enough",[15,1068,1069,1070,1073],{},"For time-based, composable, or filtered event streams, the Rx.NET ",[38,1071,1072],{},"IObservable\u003CT>"," model\nis more powerful:",[31,1075,1077],{"className":33,"code":1076,"language":35,"meta":36,"style":36},"\u002F\u002F Reactive extensions — composable event streams:\nIObservable\u003Cdecimal> prices = Observable\n    .Interval(TimeSpan.FromMilliseconds(100))\n    .Select(_ => (decimal)(Random.Shared.NextDouble() * 100));\n\nIDisposable sub = prices\n    .Where(p => p > 80)                    \u002F\u002F filter: only high prices\n    .Buffer(TimeSpan.FromSeconds(1))       \u002F\u002F batch: 1-second windows\n    .Subscribe(batch =>\n        Console.WriteLine($\"High prices this second: {batch.Count}\"));\n\nawait Task.Delay(5000);\nsub.Dispose(); \u002F\u002F unsubscribe — cleanup\n",[38,1078,1079,1084,1089,1094,1099,1103,1108,1113,1118,1123,1128,1132,1137],{"__ignoreMap":36},[41,1080,1081],{"class":43,"line":44},[41,1082,1083],{},"\u002F\u002F Reactive extensions — composable event streams:\n",[41,1085,1086],{"class":43,"line":50},[41,1087,1088],{},"IObservable\u003Cdecimal> prices = Observable\n",[41,1090,1091],{"class":43,"line":56},[41,1092,1093],{},"    .Interval(TimeSpan.FromMilliseconds(100))\n",[41,1095,1096],{"class":43,"line":63},[41,1097,1098],{},"    .Select(_ => (decimal)(Random.Shared.NextDouble() * 100));\n",[41,1100,1101],{"class":43,"line":69},[41,1102,60],{"emptyLinePlaceholder":59},[41,1104,1105],{"class":43,"line":75},[41,1106,1107],{},"IDisposable sub = prices\n",[41,1109,1110],{"class":43,"line":81},[41,1111,1112],{},"    .Where(p => p > 80)                    \u002F\u002F filter: only high prices\n",[41,1114,1115],{"class":43,"line":86},[41,1116,1117],{},"    .Buffer(TimeSpan.FromSeconds(1))       \u002F\u002F batch: 1-second windows\n",[41,1119,1120],{"class":43,"line":92},[41,1121,1122],{},"    .Subscribe(batch =>\n",[41,1124,1125],{"class":43,"line":98},[41,1126,1127],{},"        Console.WriteLine($\"High prices this second: {batch.Count}\"));\n",[41,1129,1130],{"class":43,"line":103},[41,1131,60],{"emptyLinePlaceholder":59},[41,1133,1134],{"class":43,"line":109},[41,1135,1136],{},"await Task.Delay(5000);\n",[41,1138,1139],{"class":43,"line":115},[41,1140,1141],{},"sub.Dispose(); \u002F\u002F unsubscribe — cleanup\n",[15,1143,1144,1145,1147],{},"Use events for simple notification within a bounded component. Use ",[38,1146,1072],{}," for\nreal-time streams, throttling, debouncing, time-windowed aggregation, or merging multiple\nevent sources.",[10,1149,1151],{"id":1150},"recap","Recap",[15,1153,1154,1155,1157,1158,1160,1161,1164,1165,1167,1168,1170,1171,1173,1174,1176,1177,1180,1181,1183],{},"Delegates are type-safe function pointers; ",[38,1156,225],{},", ",[38,1159,229],{},", and ",[38,1162,1163],{},"Predicate"," cover most\nneeds without custom declarations. All delegates are multicast — ",[38,1166,382],{}," and ",[38,1169,386],{}," manage the\ninvocation list. Events restrict delegate access: only the declaring class can invoke or\nassign; subscribers can only add or remove handlers. Use ",[38,1172,509],{}," with\na ",[38,1175,517],{}," raise method as the standard event pattern. Beware the\nloop-capture bug: copy loop variables before closing over them in lambdas. Always\nunsubscribe from events in ",[38,1178,1179],{},"Dispose"," when the subscriber is shorter-lived than the\npublisher — this is the most common event-related memory leak. For complex, composable\nevent streams, prefer ",[38,1182,1072],{}," from Rx.NET over raw events.",[1185,1186,1187],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":36,"searchDepth":50,"depth":50,"links":1189},[1190,1191,1192,1193,1194,1195,1197,1198,1199,1200,1202],{"id":12,"depth":50,"text":13},{"id":20,"depth":50,"text":21},{"id":124,"depth":50,"text":125},{"id":233,"depth":50,"text":234},{"id":371,"depth":50,"text":372},{"id":498,"depth":50,"text":1196},"The standard EventHandler pattern",{"id":670,"depth":50,"text":671},{"id":761,"depth":50,"text":762},{"id":954,"depth":50,"text":955},{"id":1061,"depth":50,"text":1201},"IObservable — when events aren't enough",{"id":1150,"depth":50,"text":1151},"How C# delegates and events work — multicast invocation, the EventHandler pattern, the memory leak you get when you forget to unsubscribe, and when to reach for IObservable instead.","medium","md",".NET Core","dotnet",{},"\u002Fblog\u002Fdotnet-delegates-events-observer-pattern","\u002Fdotnet\u002Fcsharp-core\u002Fdelegates-events",{"title":5,"description":1203},"blog\u002Fdotnet-delegates-events-observer-pattern","Delegates & Events","C# Core","csharp-core","2026-06-23","gbfqpb55sx7GImJllSotm6HrRgD4D9JSLuxdJk3mT4g",1782244085968]