[{"data":1,"prerenderedAt":1293},["ShallowReactive",2],{"blog-\u002Fblog\u002Fdotnet-generics-type-constraints-covariance":3},{"id":4,"title":5,"body":6,"description":1278,"difficulty":1279,"extension":1280,"framework":1281,"frameworkSlug":1282,"meta":1283,"navigation":111,"order":102,"path":1284,"qaPath":1285,"seo":1286,"stem":1287,"subtopic":1288,"topic":1289,"topicSlug":1290,"updated":1291,"__hash__":1292},"blog\u002Fblog\u002Fdotnet-generics-type-constraints-covariance.md","C# Generics: Type Constraints, Covariance, and Contravariance",{"type":7,"value":8,"toc":1267},"minimark",[9,14,53,57,77,160,186,190,204,282,285,385,396,448,452,474,532,561,581,612,615,682,686,704,711,759,772,827,831,834,959,969,973,976,984,1012,1050,1062,1109,1117,1121,1124,1132,1171,1187,1191,1263],[10,11,13],"h2",{"id":12},"why-generics-come-up-in-senior-c-interviews","Why generics come up in senior C# interviews",[15,16,17,18,22,23,22,26,29,30,22,33,36,37,40,41,44,45,48,49,52],"p",{},"Generics are everywhere in .NET — ",[19,20,21],"code",{},"List\u003CT>",", ",[19,24,25],{},"Dictionary\u003CTKey, TValue>",[19,27,28],{},"Task\u003CT>",",\n",[19,31,32],{},"IEnumerable\u003CT>",[19,34,35],{},"Func\u003CT, TResult>",". Most developers use them daily without thinking\nabout what the CLR is actually doing. Senior interviews probe whether you understand the\nmechanics: why does ",[19,38,39],{},"List\u003Cint>"," avoid boxing when ",[19,42,43],{},"ArrayList"," does not? Why can you\nassign ",[19,46,47],{},"IEnumerable\u003Cstring>"," to ",[19,50,51],{},"IEnumerable\u003Cobject>","? Why does generic type inference\nsometimes fail? These questions reveal genuine understanding.",[10,54,56],{"id":55},"c-generics-are-reified-not-erased","C# generics are reified — not erased",[15,58,59,60,64,65,68,69,72,73,76],{},"The fundamental difference between C# and Java generics is ",[61,62,63],"strong",{},"reification",". In Java,\ngeneric type parameters are erased at compile time — ",[19,66,67],{},"List\u003CInteger>"," and ",[19,70,71],{},"List\u003CString>","\nare the same class at runtime (just ",[19,74,75],{},"List","). In C#, the CLR maintains full type\ninformation at runtime and generates distinct JIT code per closed generic type.",[78,79,84],"pre",{"className":80,"code":81,"language":82,"meta":83,"style":83},"language-csharp shiki shiki-themes github-light github-dark","\u002F\u002F Each closed generic type is a distinct runtime type:\nvar intList    = new List\u003Cint>();\nvar stringList = new List\u003Cstring>();\n\nConsole.WriteLine(intList.GetType()    == stringList.GetType()); \u002F\u002F False\nConsole.WriteLine(intList.GetType().Name);                        \u002F\u002F \"List`1\"\nConsole.WriteLine(intList.GetType().GenericTypeArguments[0]);     \u002F\u002F System.Int32\n\n\u002F\u002F typeof(T) works inside generic methods — impossible in Java without hacks:\nvoid PrintTypeName\u003CT>() => Console.WriteLine(typeof(T).FullName);\nPrintTypeName\u003CDateTime>(); \u002F\u002F \"System.DateTime\"\nPrintTypeName\u003Cstring>();   \u002F\u002F \"System.String\"\n","csharp","",[19,85,86,94,100,106,113,119,125,131,136,142,148,154],{"__ignoreMap":83},[87,88,91],"span",{"class":89,"line":90},"line",1,[87,92,93],{},"\u002F\u002F Each closed generic type is a distinct runtime type:\n",[87,95,97],{"class":89,"line":96},2,[87,98,99],{},"var intList    = new List\u003Cint>();\n",[87,101,103],{"class":89,"line":102},3,[87,104,105],{},"var stringList = new List\u003Cstring>();\n",[87,107,109],{"class":89,"line":108},4,[87,110,112],{"emptyLinePlaceholder":111},true,"\n",[87,114,116],{"class":89,"line":115},5,[87,117,118],{},"Console.WriteLine(intList.GetType()    == stringList.GetType()); \u002F\u002F False\n",[87,120,122],{"class":89,"line":121},6,[87,123,124],{},"Console.WriteLine(intList.GetType().Name);                        \u002F\u002F \"List`1\"\n",[87,126,128],{"class":89,"line":127},7,[87,129,130],{},"Console.WriteLine(intList.GetType().GenericTypeArguments[0]);     \u002F\u002F System.Int32\n",[87,132,134],{"class":89,"line":133},8,[87,135,112],{"emptyLinePlaceholder":111},[87,137,139],{"class":89,"line":138},9,[87,140,141],{},"\u002F\u002F typeof(T) works inside generic methods — impossible in Java without hacks:\n",[87,143,145],{"class":89,"line":144},10,[87,146,147],{},"void PrintTypeName\u003CT>() => Console.WriteLine(typeof(T).FullName);\n",[87,149,151],{"class":89,"line":150},11,[87,152,153],{},"PrintTypeName\u003CDateTime>(); \u002F\u002F \"System.DateTime\"\n",[87,155,157],{"class":89,"line":156},12,[87,158,159],{},"PrintTypeName\u003Cstring>();   \u002F\u002F \"System.String\"\n",[15,161,162,163,166,167,170,171,173,174,177,178,181,182,185],{},"For ",[61,164,165],{},"reference type"," arguments (class types), the JIT shares a single native code\nimplementation across all reference-type specialisations (since all references are\nthe same size — one pointer). For ",[61,168,169],{},"value type"," arguments (structs), the JIT generates\na distinct native implementation per value type. This is why ",[19,172,39],{}," stores ",[19,175,176],{},"int","\nvalues directly in its internal array without boxing — the JIT produces code that\noperates on ",[19,179,180],{},"int[]"," directly, not ",[19,183,184],{},"object[]",".",[10,187,189],{"id":188},"type-constraints-unlocking-ts-members","Type constraints — unlocking T's members",[15,191,192,193,196,197,200,201,203],{},"Without constraints, ",[19,194,195],{},"T"," is opaque — you can only call methods on ",[19,198,199],{},"System.Object",".\nConstraints narrow ",[19,202,195],{}," to a set of types, unlocking their members inside the generic.",[78,205,207],{"className":80,"code":206,"language":82,"meta":83,"style":83},"\u002F\u002F No constraint — only Object members available:\nT Echo\u003CT>(T value)\n{\n    Console.WriteLine(value.ToString()); \u002F\u002F ToString() comes from object — fine\n    \u002F\u002F value.CompareTo(value);           \u002F\u002F not on object\n    return value;\n}\n\n\u002F\u002F Constrained — CompareTo() now available:\nT Max\u003CT>(T a, T b) where T : IComparable\u003CT>\n    => a.CompareTo(b) >= 0 ? a : b;\n\nConsole.WriteLine(Max(3, 7));         \u002F\u002F 7\nConsole.WriteLine(Max(\"apple\", \"fig\")); \u002F\u002F \"fig\"\n",[19,208,209,214,219,224,229,237,242,247,251,256,261,266,270,276],{"__ignoreMap":83},[87,210,211],{"class":89,"line":90},[87,212,213],{},"\u002F\u002F No constraint — only Object members available:\n",[87,215,216],{"class":89,"line":96},[87,217,218],{},"T Echo\u003CT>(T value)\n",[87,220,221],{"class":89,"line":102},[87,222,223],{},"{\n",[87,225,226],{"class":89,"line":108},[87,227,228],{},"    Console.WriteLine(value.ToString()); \u002F\u002F ToString() comes from object — fine\n",[87,230,231,234],{"class":89,"line":115},[87,232,233],{},"    \u002F\u002F value.CompareTo(value);",[87,235,236],{},"           \u002F\u002F not on object\n",[87,238,239],{"class":89,"line":121},[87,240,241],{},"    return value;\n",[87,243,244],{"class":89,"line":127},[87,245,246],{},"}\n",[87,248,249],{"class":89,"line":133},[87,250,112],{"emptyLinePlaceholder":111},[87,252,253],{"class":89,"line":138},[87,254,255],{},"\u002F\u002F Constrained — CompareTo() now available:\n",[87,257,258],{"class":89,"line":144},[87,259,260],{},"T Max\u003CT>(T a, T b) where T : IComparable\u003CT>\n",[87,262,263],{"class":89,"line":150},[87,264,265],{},"    => a.CompareTo(b) >= 0 ? a : b;\n",[87,267,268],{"class":89,"line":156},[87,269,112],{"emptyLinePlaceholder":111},[87,271,273],{"class":89,"line":272},13,[87,274,275],{},"Console.WriteLine(Max(3, 7));         \u002F\u002F 7\n",[87,277,279],{"class":89,"line":278},14,[87,280,281],{},"Console.WriteLine(Max(\"apple\", \"fig\")); \u002F\u002F \"fig\"\n",[15,283,284],{},"The full menu of constraints:",[286,287,288,301],"table",{},[289,290,291],"thead",{},[292,293,294,298],"tr",{},[295,296,297],"th",{},"Constraint",[295,299,300],{},"Meaning",[302,303,304,315,325,335,345,355,365,375],"tbody",{},[292,305,306,312],{},[307,308,309],"td",{},[19,310,311],{},"where T : class",[307,313,314],{},"T must be a reference type",[292,316,317,322],{},[307,318,319],{},[19,320,321],{},"where T : struct",[307,323,324],{},"T must be a non-nullable value type",[292,326,327,332],{},[307,328,329],{},[19,330,331],{},"where T : notnull",[307,333,334],{},"T must be non-nullable (class or non-nullable struct)",[292,336,337,342],{},[307,338,339],{},[19,340,341],{},"where T : new()",[307,343,344],{},"T must have a public parameterless constructor",[292,346,347,352],{},[307,348,349],{},[19,350,351],{},"where T : SomeClass",[307,353,354],{},"T must be SomeClass or derive from it",[292,356,357,362],{},[307,358,359],{},[19,360,361],{},"where T : ISomeInterface",[307,363,364],{},"T must implement the interface",[292,366,367,372],{},[307,368,369],{},[19,370,371],{},"where T : unmanaged",[307,373,374],{},"T must be an unmanaged type (C# 7.3+)",[292,376,377,382],{},[307,378,379],{},[19,380,381],{},"where T : default",[307,383,384],{},"T can be both nullable and non-nullable (C# 9+, for method overrides)",[15,386,387,388,391,392,395],{},"Multiple constraints on one parameter are comma-separated; ",[19,389,390],{},"class"," or ",[19,393,394],{},"struct"," must\ncome first:",[78,397,399],{"className":80,"code":398,"language":82,"meta":83,"style":83},"class Repository\u003CT> where T : class, new()\n{\n    public T CreateEmpty() => new T(); \u002F\u002F new() enables this\n}\n\n\u002F\u002F Multiple type parameters, each independently constrained:\nclass Pair\u003CTFirst, TSecond>\n    where TFirst  : IComparable\u003CTFirst>\n    where TSecond : class, new()\n{ \u002F* ... *\u002F }\n",[19,400,401,406,410,415,419,423,428,433,438,443],{"__ignoreMap":83},[87,402,403],{"class":89,"line":90},[87,404,405],{},"class Repository\u003CT> where T : class, new()\n",[87,407,408],{"class":89,"line":96},[87,409,223],{},[87,411,412],{"class":89,"line":102},[87,413,414],{},"    public T CreateEmpty() => new T(); \u002F\u002F new() enables this\n",[87,416,417],{"class":89,"line":108},[87,418,246],{},[87,420,421],{"class":89,"line":115},[87,422,112],{"emptyLinePlaceholder":111},[87,424,425],{"class":89,"line":121},[87,426,427],{},"\u002F\u002F Multiple type parameters, each independently constrained:\n",[87,429,430],{"class":89,"line":127},[87,431,432],{},"class Pair\u003CTFirst, TSecond>\n",[87,434,435],{"class":89,"line":133},[87,436,437],{},"    where TFirst  : IComparable\u003CTFirst>\n",[87,439,440],{"class":89,"line":138},[87,441,442],{},"    where TSecond : class, new()\n",[87,444,445],{"class":89,"line":144},[87,446,447],{},"{ \u002F* ... *\u002F }\n",[10,449,451],{"id":450},"covariance-safe-upcasting-for-producers","Covariance — safe upcasting for producers",[15,453,454,457,458,461,462,465,466,469,470,473],{},[61,455,456],{},"Covariance"," means a ",[19,459,460],{},"Generic\u003CDerived>"," is assignable to ",[19,463,464],{},"Generic\u003CBase>"," when the\ntype parameter only appears in ",[61,467,468],{},"output"," (producer) positions. In C# this is expressed\nwith the ",[19,471,472],{},"out"," keyword on interface or delegate type parameters.",[78,475,477],{"className":80,"code":476,"language":82,"meta":83,"style":83},"\u002F\u002F IEnumerable\u003CT> is declared covariant: interface IEnumerable\u003Cout T>\nIEnumerable\u003Cstring>  strings = new List\u003Cstring> { \"a\", \"b\" };\nIEnumerable\u003Cobject>  objects = strings;  \u002F\u002F covariance — compiles fine\n\n\u002F\u002F Safe because you can only READ from IEnumerable\u003CT>:\nforeach (object obj in objects)\n    Console.WriteLine(obj.GetType().Name); \u002F\u002F String, String\n\n\u002F\u002F IReadOnlyList\u003CT> is also covariant:\nIReadOnlyList\u003Cstring> strList = new[] { \"x\", \"y\" };\nIReadOnlyList\u003Cobject> objList = strList; \u002F\u002F valid\n",[19,478,479,484,489,494,498,503,508,513,517,522,527],{"__ignoreMap":83},[87,480,481],{"class":89,"line":90},[87,482,483],{},"\u002F\u002F IEnumerable\u003CT> is declared covariant: interface IEnumerable\u003Cout T>\n",[87,485,486],{"class":89,"line":96},[87,487,488],{},"IEnumerable\u003Cstring>  strings = new List\u003Cstring> { \"a\", \"b\" };\n",[87,490,491],{"class":89,"line":102},[87,492,493],{},"IEnumerable\u003Cobject>  objects = strings;  \u002F\u002F covariance — compiles fine\n",[87,495,496],{"class":89,"line":108},[87,497,112],{"emptyLinePlaceholder":111},[87,499,500],{"class":89,"line":115},[87,501,502],{},"\u002F\u002F Safe because you can only READ from IEnumerable\u003CT>:\n",[87,504,505],{"class":89,"line":121},[87,506,507],{},"foreach (object obj in objects)\n",[87,509,510],{"class":89,"line":127},[87,511,512],{},"    Console.WriteLine(obj.GetType().Name); \u002F\u002F String, String\n",[87,514,515],{"class":89,"line":133},[87,516,112],{"emptyLinePlaceholder":111},[87,518,519],{"class":89,"line":138},[87,520,521],{},"\u002F\u002F IReadOnlyList\u003CT> is also covariant:\n",[87,523,524],{"class":89,"line":144},[87,525,526],{},"IReadOnlyList\u003Cstring> strList = new[] { \"x\", \"y\" };\n",[87,528,529],{"class":89,"line":150},[87,530,531],{},"IReadOnlyList\u003Cobject> objList = strList; \u002F\u002F valid\n",[15,533,534,535,537,538,540,541,544,545,548,549,553,554,557,558,560],{},"Why is it safe? When you read from an ",[19,536,47],{}," through the ",[19,539,51],{},"\nreference, every value you get out is guaranteed to be a ",[19,542,543],{},"string",", which is a valid\n",[19,546,547],{},"object",". You cannot put anything ",[550,551,552],"em",{},"in"," — there is no ",[19,555,556],{},"Add"," method on ",[19,559,32],{}," —\nso there is no way to introduce a non-string into the collection.",[15,562,563,564,567,568,571,572,68,574,577,578,580],{},"Covariance would be ",[61,565,566],{},"unsafe"," for ",[19,569,570],{},"IList\u003CT>"," (which has ",[19,573,556],{},[19,575,576],{},"this[int] = value","),\nwhich is why ",[19,579,570],{}," is invariant. The compiler enforces this:",[78,582,584],{"className":80,"code":583,"language":82,"meta":83,"style":83},"IList\u003Cstring> strList2 = new List\u003Cstring> { \"a\" };\n\u002F\u002F IList\u003Cobject> objList2 = strList2; \u002F\u002F compile error — IList\u003CT> is invariant\n\u002F\u002F Without this restriction:\n\u002F\u002F objList2.Add(42); \u002F\u002F would add an int to a List\u003Cstring> → runtime catastrophe\n",[19,585,586,591,599,604],{"__ignoreMap":83},[87,587,588],{"class":89,"line":90},[87,589,590],{},"IList\u003Cstring> strList2 = new List\u003Cstring> { \"a\" };\n",[87,592,593,596],{"class":89,"line":96},[87,594,595],{},"\u002F\u002F IList\u003Cobject> objList2 = strList2;",[87,597,598],{}," \u002F\u002F compile error — IList\u003CT> is invariant\n",[87,600,601],{"class":89,"line":102},[87,602,603],{},"\u002F\u002F Without this restriction:\n",[87,605,606,609],{"class":89,"line":108},[87,607,608],{},"\u002F\u002F objList2.Add(42);",[87,610,611],{}," \u002F\u002F would add an int to a List\u003Cstring> → runtime catastrophe\n",[15,613,614],{},"Defining your own covariant interface:",[78,616,618],{"className":80,"code":617,"language":82,"meta":83,"style":83},"interface IProducer\u003Cout T>  \u002F\u002F 'out' = covariant\n{\n    T Produce();             \u002F\u002F T only in output (return) position \n    \u002F\u002F void Consume(T item); \u002F\u002F T in input position — compiler rejects\n}\n\nclass StringProducer : IProducer\u003Cstring>\n{\n    public string Produce() => \"hello\";\n}\n\nIProducer\u003Cstring> sp = new StringProducer();\nIProducer\u003Cobject> op = sp; \u002F\u002F covariance — safe\n",[19,619,620,625,629,634,642,646,650,655,659,664,668,672,677],{"__ignoreMap":83},[87,621,622],{"class":89,"line":90},[87,623,624],{},"interface IProducer\u003Cout T>  \u002F\u002F 'out' = covariant\n",[87,626,627],{"class":89,"line":96},[87,628,223],{},[87,630,631],{"class":89,"line":102},[87,632,633],{},"    T Produce();             \u002F\u002F T only in output (return) position \n",[87,635,636,639],{"class":89,"line":108},[87,637,638],{},"    \u002F\u002F void Consume(T item);",[87,640,641],{}," \u002F\u002F T in input position — compiler rejects\n",[87,643,644],{"class":89,"line":115},[87,645,246],{},[87,647,648],{"class":89,"line":121},[87,649,112],{"emptyLinePlaceholder":111},[87,651,652],{"class":89,"line":127},[87,653,654],{},"class StringProducer : IProducer\u003Cstring>\n",[87,656,657],{"class":89,"line":133},[87,658,223],{},[87,660,661],{"class":89,"line":138},[87,662,663],{},"    public string Produce() => \"hello\";\n",[87,665,666],{"class":89,"line":144},[87,667,246],{},[87,669,670],{"class":89,"line":150},[87,671,112],{"emptyLinePlaceholder":111},[87,673,674],{"class":89,"line":156},[87,675,676],{},"IProducer\u003Cstring> sp = new StringProducer();\n",[87,678,679],{"class":89,"line":272},[87,680,681],{},"IProducer\u003Cobject> op = sp; \u002F\u002F covariance — safe\n",[10,683,685],{"id":684},"contravariance-safe-downcasting-for-consumers","Contravariance — safe downcasting for consumers",[15,687,688,691,692,461,694,696,697,700,701,703],{},[61,689,690],{},"Contravariance"," is the reverse: a ",[19,693,464],{},[19,695,460],{},"\nwhen the type parameter only appears in ",[61,698,699],{},"input"," (consumer) positions. Expressed with\nthe ",[19,702,552],{}," keyword.",[15,705,706,707,710],{},"The classic example is ",[19,708,709],{},"Action\u003CT>",":",[78,712,714],{"className":80,"code":713,"language":82,"meta":83,"style":83},"\u002F\u002F Action\u003CT> is declared contravariant: delegate void Action\u003Cin T>(T obj)\nAction\u003Cobject> handleObject = obj => Console.WriteLine(obj);\nAction\u003Cstring> handleString = handleObject; \u002F\u002F contravariance — compiles fine\n\nhandleString(\"hello\"); \u002F\u002F calls handleObject with \"hello\" — string is-a object \n\n\u002F\u002F Why is this safe?\n\u002F\u002F A method that can handle ANY object can certainly handle a string.\n\u002F\u002F Contravariance lets a \"general\" handler substitute for a \"specific\" one.\n",[19,715,716,721,726,731,735,740,744,749,754],{"__ignoreMap":83},[87,717,718],{"class":89,"line":90},[87,719,720],{},"\u002F\u002F Action\u003CT> is declared contravariant: delegate void Action\u003Cin T>(T obj)\n",[87,722,723],{"class":89,"line":96},[87,724,725],{},"Action\u003Cobject> handleObject = obj => Console.WriteLine(obj);\n",[87,727,728],{"class":89,"line":102},[87,729,730],{},"Action\u003Cstring> handleString = handleObject; \u002F\u002F contravariance — compiles fine\n",[87,732,733],{"class":89,"line":108},[87,734,112],{"emptyLinePlaceholder":111},[87,736,737],{"class":89,"line":115},[87,738,739],{},"handleString(\"hello\"); \u002F\u002F calls handleObject with \"hello\" — string is-a object \n",[87,741,742],{"class":89,"line":121},[87,743,112],{"emptyLinePlaceholder":111},[87,745,746],{"class":89,"line":127},[87,747,748],{},"\u002F\u002F Why is this safe?\n",[87,750,751],{"class":89,"line":133},[87,752,753],{},"\u002F\u002F A method that can handle ANY object can certainly handle a string.\n",[87,755,756],{"class":89,"line":138},[87,757,758],{},"\u002F\u002F Contravariance lets a \"general\" handler substitute for a \"specific\" one.\n",[15,760,761,762,765,766,768,769,771],{},"The intuition: for consumers (things that ",[550,763,764],{},"accept"," T), a handler for the base type is\nalways compatible with a handler for the derived type. If you can process ",[19,767,547],{},",\nyou can process ",[19,770,543],{}," — but not necessarily the other way around.",[78,773,775],{"className":80,"code":774,"language":82,"meta":83,"style":83},"\u002F\u002F IComparer\u003CT> is contravariant:\nIComparer\u003Cobject> objCmp = Comparer\u003Cobject>.Default;\nIComparer\u003Cstring> strCmp = objCmp; \u002F\u002F valid — comparer for object works for string\n\n\u002F\u002F Your own contravariant interface:\ninterface IConsumer\u003Cin T>\n{\n    void Consume(T item);    \u002F\u002F T only in input (parameter) position \n    \u002F\u002F T Produce();          \u002F\u002F T in output position — compiler rejects\n}\n",[19,776,777,782,787,792,796,801,806,810,815,823],{"__ignoreMap":83},[87,778,779],{"class":89,"line":90},[87,780,781],{},"\u002F\u002F IComparer\u003CT> is contravariant:\n",[87,783,784],{"class":89,"line":96},[87,785,786],{},"IComparer\u003Cobject> objCmp = Comparer\u003Cobject>.Default;\n",[87,788,789],{"class":89,"line":102},[87,790,791],{},"IComparer\u003Cstring> strCmp = objCmp; \u002F\u002F valid — comparer for object works for string\n",[87,793,794],{"class":89,"line":108},[87,795,112],{"emptyLinePlaceholder":111},[87,797,798],{"class":89,"line":115},[87,799,800],{},"\u002F\u002F Your own contravariant interface:\n",[87,802,803],{"class":89,"line":121},[87,804,805],{},"interface IConsumer\u003Cin T>\n",[87,807,808],{"class":89,"line":127},[87,809,223],{},[87,811,812],{"class":89,"line":133},[87,813,814],{},"    void Consume(T item);    \u002F\u002F T only in input (parameter) position \n",[87,816,817,820],{"class":89,"line":138},[87,818,819],{},"    \u002F\u002F T Produce();",[87,821,822],{},"          \u002F\u002F T in output position — compiler rejects\n",[87,824,825],{"class":89,"line":144},[87,826,246],{},[10,828,830],{"id":829},"generic-delegates-action-func-predicate","Generic delegates — Action, Func, Predicate",[15,832,833],{},"The BCL provides built-in generic delegate types that cover the vast majority of\ncallback scenarios without requiring custom delegate declarations.",[78,835,837],{"className":80,"code":836,"language":82,"meta":83,"style":83},"\u002F\u002F Action\u003CT...> — 0–16 parameters, void return\nAction          doSomething = () => Console.WriteLine(\"go\");\nAction\u003Cstring>  log         = msg => Console.WriteLine($\"[LOG] {msg}\");\nAction\u003Cint,int> add         = (a, b) => Console.WriteLine(a + b);\n\n\u002F\u002F Func\u003CT..., TResult> — 0–16 input params, returns TResult\nFunc\u003Cint>          getZero  = () => 0;\nFunc\u003Cint, string>  intToStr = n => n.ToString();\nFunc\u003Cint,int,int>  multiply = (a, b) => a * b;\nConsole.WriteLine(multiply(6, 7)); \u002F\u002F 42\n\n\u002F\u002F Predicate\u003CT> — exactly Func\u003CT, bool>; used in List\u003CT>.Find etc.\nPredicate\u003Cint> isEven = n => n % 2 == 0;\nvar list = new List\u003Cint> { 1, 2, 3, 4 };\nint found = list.Find(isEven); \u002F\u002F 2\n\n\u002F\u002F Covariance in Func — Func\u003Cout TResult> is covariant in TResult:\nFunc\u003Cstring> getStr = () => \"hello\";\nFunc\u003Cobject> getObj = getStr; \u002F\u002F covariance — string is-a object\n\n\u002F\u002F Contravariance in Action — Action\u003Cin T> is contravariant in T:\nAction\u003Cobject> printObj = obj => Console.WriteLine(obj);\nAction\u003Cstring> printStr = printObj; \u002F\u002F contravariance\n",[19,838,839,844,849,854,859,863,868,873,878,883,888,892,897,902,907,913,918,924,930,936,941,947,953],{"__ignoreMap":83},[87,840,841],{"class":89,"line":90},[87,842,843],{},"\u002F\u002F Action\u003CT...> — 0–16 parameters, void return\n",[87,845,846],{"class":89,"line":96},[87,847,848],{},"Action          doSomething = () => Console.WriteLine(\"go\");\n",[87,850,851],{"class":89,"line":102},[87,852,853],{},"Action\u003Cstring>  log         = msg => Console.WriteLine($\"[LOG] {msg}\");\n",[87,855,856],{"class":89,"line":108},[87,857,858],{},"Action\u003Cint,int> add         = (a, b) => Console.WriteLine(a + b);\n",[87,860,861],{"class":89,"line":115},[87,862,112],{"emptyLinePlaceholder":111},[87,864,865],{"class":89,"line":121},[87,866,867],{},"\u002F\u002F Func\u003CT..., TResult> — 0–16 input params, returns TResult\n",[87,869,870],{"class":89,"line":127},[87,871,872],{},"Func\u003Cint>          getZero  = () => 0;\n",[87,874,875],{"class":89,"line":133},[87,876,877],{},"Func\u003Cint, string>  intToStr = n => n.ToString();\n",[87,879,880],{"class":89,"line":138},[87,881,882],{},"Func\u003Cint,int,int>  multiply = (a, b) => a * b;\n",[87,884,885],{"class":89,"line":144},[87,886,887],{},"Console.WriteLine(multiply(6, 7)); \u002F\u002F 42\n",[87,889,890],{"class":89,"line":150},[87,891,112],{"emptyLinePlaceholder":111},[87,893,894],{"class":89,"line":156},[87,895,896],{},"\u002F\u002F Predicate\u003CT> — exactly Func\u003CT, bool>; used in List\u003CT>.Find etc.\n",[87,898,899],{"class":89,"line":272},[87,900,901],{},"Predicate\u003Cint> isEven = n => n % 2 == 0;\n",[87,903,904],{"class":89,"line":278},[87,905,906],{},"var list = new List\u003Cint> { 1, 2, 3, 4 };\n",[87,908,910],{"class":89,"line":909},15,[87,911,912],{},"int found = list.Find(isEven); \u002F\u002F 2\n",[87,914,916],{"class":89,"line":915},16,[87,917,112],{"emptyLinePlaceholder":111},[87,919,921],{"class":89,"line":920},17,[87,922,923],{},"\u002F\u002F Covariance in Func — Func\u003Cout TResult> is covariant in TResult:\n",[87,925,927],{"class":89,"line":926},18,[87,928,929],{},"Func\u003Cstring> getStr = () => \"hello\";\n",[87,931,933],{"class":89,"line":932},19,[87,934,935],{},"Func\u003Cobject> getObj = getStr; \u002F\u002F covariance — string is-a object\n",[87,937,939],{"class":89,"line":938},20,[87,940,112],{"emptyLinePlaceholder":111},[87,942,944],{"class":89,"line":943},21,[87,945,946],{},"\u002F\u002F Contravariance in Action — Action\u003Cin T> is contravariant in T:\n",[87,948,950],{"class":89,"line":949},22,[87,951,952],{},"Action\u003Cobject> printObj = obj => Console.WriteLine(obj);\n",[87,954,956],{"class":89,"line":955},23,[87,957,958],{},"Action\u003Cstring> printStr = printObj; \u002F\u002F contravariance\n",[15,960,961,962,68,965,968],{},"Never define a custom delegate type for the standard cases — ",[19,963,964],{},"Action",[19,966,967],{},"Func","\ncompose well with LINQ, TPL, and every .NET API. Only define a named delegate when\nyou need a more descriptive type name for a public API or when the parameter count\nexceeds 16 (extremely rare).",[10,970,972],{"id":971},"defaultt-and-typeoft","default(T) and typeof(T)",[15,974,975],{},"Two operators are invaluable inside generic methods:",[15,977,978,983],{},[61,979,980],{},[19,981,982],{},"default(T)"," — returns the zero\u002Fnull value for any type parameter:",[985,986,987,994,1000,1009],"ul",{},[988,989,990,991],"li",{},"Reference types → ",[19,992,993],{},"null",[988,995,996,997],{},"Numeric value types → ",[19,998,999],{},"0",[988,1001,1002,1005,1006],{},[19,1003,1004],{},"bool"," → ",[19,1007,1008],{},"false",[988,1010,1011],{},"Any struct → zero-initialised instance",[78,1013,1015],{"className":80,"code":1014,"language":82,"meta":83,"style":83},"T[] Fill\u003CT>(int count, T value = default!)\n{\n    var arr = new T[count];\n    if (!EqualityComparer\u003CT>.Default.Equals(value, default))\n        Array.Fill(arr, value);\n    return arr;\n}\n",[19,1016,1017,1022,1026,1031,1036,1041,1046],{"__ignoreMap":83},[87,1018,1019],{"class":89,"line":90},[87,1020,1021],{},"T[] Fill\u003CT>(int count, T value = default!)\n",[87,1023,1024],{"class":89,"line":96},[87,1025,223],{},[87,1027,1028],{"class":89,"line":102},[87,1029,1030],{},"    var arr = new T[count];\n",[87,1032,1033],{"class":89,"line":108},[87,1034,1035],{},"    if (!EqualityComparer\u003CT>.Default.Equals(value, default))\n",[87,1037,1038],{"class":89,"line":115},[87,1039,1040],{},"        Array.Fill(arr, value);\n",[87,1042,1043],{"class":89,"line":121},[87,1044,1045],{},"    return arr;\n",[87,1047,1048],{"class":89,"line":127},[87,1049,246],{},[15,1051,1052,1057,1058,1061],{},[61,1053,1054],{},[19,1055,1056],{},"typeof(T)"," — returns the ",[19,1059,1060],{},"System.Type"," object for the static type parameter:",[78,1063,1065],{"className":80,"code":1064,"language":82,"meta":83,"style":83},"bool IsNullable\u003CT>()\n{\n    \u002F\u002F Nullable\u003CT> is a value type, and typeof(T) is available at runtime:\n    return Nullable.GetUnderlyingType(typeof(T)) != null;\n}\n\nConsole.WriteLine(IsNullable\u003Cint?>());  \u002F\u002F True\nConsole.WriteLine(IsNullable\u003Cint>());   \u002F\u002F False\nConsole.WriteLine(IsNullable\u003Cstring>()); \u002F\u002F False (string is a ref type, not Nullable\u003CT>)\n",[19,1066,1067,1072,1076,1081,1086,1090,1094,1099,1104],{"__ignoreMap":83},[87,1068,1069],{"class":89,"line":90},[87,1070,1071],{},"bool IsNullable\u003CT>()\n",[87,1073,1074],{"class":89,"line":96},[87,1075,223],{},[87,1077,1078],{"class":89,"line":102},[87,1079,1080],{},"    \u002F\u002F Nullable\u003CT> is a value type, and typeof(T) is available at runtime:\n",[87,1082,1083],{"class":89,"line":108},[87,1084,1085],{},"    return Nullable.GetUnderlyingType(typeof(T)) != null;\n",[87,1087,1088],{"class":89,"line":115},[87,1089,246],{},[87,1091,1092],{"class":89,"line":121},[87,1093,112],{"emptyLinePlaceholder":111},[87,1095,1096],{"class":89,"line":127},[87,1097,1098],{},"Console.WriteLine(IsNullable\u003Cint?>());  \u002F\u002F True\n",[87,1100,1101],{"class":89,"line":133},[87,1102,1103],{},"Console.WriteLine(IsNullable\u003Cint>());   \u002F\u002F False\n",[87,1105,1106],{"class":89,"line":138},[87,1107,1108],{},"Console.WriteLine(IsNullable\u003Cstring>()); \u002F\u002F False (string is a ref type, not Nullable\u003CT>)\n",[15,1110,1111,1112,68,1114,1116],{},"Both ",[19,1113,982],{},[19,1115,1056],{}," work because C# generics are reified — the type\nparameter is a real type at runtime, not erased. Neither has an equivalent in Java\nwithout reflection hacks.",[10,1118,1120],{"id":1119},"ienumerablet-vs-icollectiont-vs-ilistt","IEnumerable\u003CT> vs ICollection\u003CT> vs IList\u003CT>",[15,1122,1123],{},"The generic collection interface hierarchy is worth knowing cold for interviews:",[78,1125,1130],{"className":1126,"code":1128,"language":1129},[1127],"language-text","IEnumerable\u003CT>       — iterate only (foreach)\n    └─ ICollection\u003CT>  — + Count, Add, Remove, Contains, Clear\n           └─ IList\u003CT>   — + indexer this[int], Insert, RemoveAt\n","text",[19,1131,1128],{"__ignoreMap":83},[78,1133,1135],{"className":80,"code":1134,"language":82,"meta":83,"style":83},"\u002F\u002F Accept the narrowest interface your implementation needs:\nvoid Print\u003CT>(IEnumerable\u003CT> items)    \u002F\u002F widest — accepts arrays, List\u003CT>, HashSet\u003CT>, LINQ queries\nvoid AddItem\u003CT>(ICollection\u003CT> col)    \u002F\u002F needs Add\nvoid SwapFirst\u003CT>(IList\u003CT> list)       \u002F\u002F needs index access\n\n\u002F\u002F Return concrete types so callers get the full API:\nList\u003Cstring> GetNames() => new List\u003Cstring> { \"Alice\", \"Bob\" };\n",[19,1136,1137,1142,1147,1152,1157,1161,1166],{"__ignoreMap":83},[87,1138,1139],{"class":89,"line":90},[87,1140,1141],{},"\u002F\u002F Accept the narrowest interface your implementation needs:\n",[87,1143,1144],{"class":89,"line":96},[87,1145,1146],{},"void Print\u003CT>(IEnumerable\u003CT> items)    \u002F\u002F widest — accepts arrays, List\u003CT>, HashSet\u003CT>, LINQ queries\n",[87,1148,1149],{"class":89,"line":102},[87,1150,1151],{},"void AddItem\u003CT>(ICollection\u003CT> col)    \u002F\u002F needs Add\n",[87,1153,1154],{"class":89,"line":108},[87,1155,1156],{},"void SwapFirst\u003CT>(IList\u003CT> list)       \u002F\u002F needs index access\n",[87,1158,1159],{"class":89,"line":115},[87,1160,112],{"emptyLinePlaceholder":111},[87,1162,1163],{"class":89,"line":121},[87,1164,1165],{},"\u002F\u002F Return concrete types so callers get the full API:\n",[87,1167,1168],{"class":89,"line":127},[87,1169,1170],{},"List\u003Cstring> GetNames() => new List\u003Cstring> { \"Alice\", \"Bob\" };\n",[15,1172,1173,1176,1177,68,1180,1182,1183,1186],{},[19,1174,1175],{},"IEnumerable\u003Cout T>"," is covariant; ",[19,1178,1179],{},"ICollection\u003CT>",[19,1181,570],{}," are invariant\n(because they have ",[19,1184,1185],{},"Add(T)"," — an input position).",[10,1188,1190],{"id":1189},"recap","Recap",[15,1192,1193,1194,1197,1198,29,1200,1203,1204,1206,1207,1210,1211,1214,1215,1217,1218,1210,1220,1223,1224,1226,1227,1229,1230,1232,1233,1210,1236,1223,1239,1241,1242,1244,1245,1247,1248,22,1250,1253,1254,1257,1258,68,1260,1262],{},"C# generics are ",[61,1195,1196],{},"reified"," at runtime — each closed generic type (",[19,1199,39],{},[19,1201,1202],{},"List\u003Cstring>",") is a distinct CLR type with its own JIT-compiled code and full type\ninformation available via reflection. This eliminates boxing for value-type specialisations\nand makes ",[19,1205,1056],{}," work inside generic methods. ",[61,1208,1209],{},"Type constraints"," (",[19,1212,1213],{},"where T : ...",")\nunlock members of ",[19,1216,195],{}," inside the generic and communicate requirements to callers.\n",[61,1219,456],{},[19,1221,1222],{},"out T",") allows ",[19,1225,460],{}," to be used as ",[19,1228,464],{}," when\n",[19,1231,195],{}," is only produced (output); ",[61,1234,1235],{},"contravariance",[19,1237,1238],{},"in T",[19,1240,464],{}," to\nbe used as ",[19,1243,460],{}," when ",[19,1246,195],{}," is only consumed (input). The built-in\n",[19,1249,709],{},[19,1251,1252],{},"Func\u003CT,TResult>",", and ",[19,1255,1256],{},"Predicate\u003CT>"," delegates cover nearly all callback\nneeds without requiring custom delegate declarations. ",[19,1259,982],{},[19,1261,1056],{}," are\nthe two generic-specific operators you reach for when the compiler cannot infer from\ncontext.",[1264,1265,1266],"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":83,"searchDepth":96,"depth":96,"links":1268},[1269,1270,1271,1272,1273,1274,1275,1276,1277],{"id":12,"depth":96,"text":13},{"id":55,"depth":96,"text":56},{"id":188,"depth":96,"text":189},{"id":450,"depth":96,"text":451},{"id":684,"depth":96,"text":685},{"id":829,"depth":96,"text":830},{"id":971,"depth":96,"text":972},{"id":1119,"depth":96,"text":1120},{"id":1189,"depth":96,"text":1190},"How C# generics work under the CLR — reification vs type erasure, constraints, variance with out\u002Fin, and why generic collections are faster than their non-generic predecessors.","medium","md",".NET Core","dotnet",{},"\u002Fblog\u002Fdotnet-generics-type-constraints-covariance","\u002Fdotnet\u002Ffundamentals\u002Fgenerics",{"title":5,"description":1278},"blog\u002Fdotnet-generics-type-constraints-covariance","Generics","Fundamentals","fundamentals","2026-06-22","lAJNtEKDQZ32hVzYstbVi9ja5oVo79ps_-nmDFel3tc",1782244087235]