about
Bits: Generic C#
07/11/2024
0
Bits Repo Code Bits Repo Docs

Bits: Generic C#

generic library and user-defined types

Synopsis:

This page demonstrates uses of C# generic functions and types.
  • C# generics support definition of class and method patterns, which can be instantiated for a variety of different concrete types.
  • Each instantiation must use types that provide all the facilities used in the pattern.
  • The pattern can provide constraints on its type parameters that guarantee methods and properties required by the pattern.
Demo Notes  
All of the languages covered in this demonstration support generics with the exception of JavaScript. Three of the languages: C++, Rust, and C# provide generics. Python provides a typing module that provides type hints, checked with a static analysis tool. Each generic function or class is a pattern for defining functions and classes of a specific type. Thus a generic is a pattern for making patterns. Python and JavaScript, are dynamically typed and already support defining functions and classes for multiple types, e.g., no need for generics. This demonstration illustrates use of generic functions and classes, which for C++, Rust, and C#, are processed by the compiler.

1.0 Generics and Constraints

Generics are patterns for functions and classes that use placeholder generic types, like T below. The patterns become specific functions and classes when the placeholders are replaced with specific defined types.
Generic functions use the syntax:
Examples in Section 2.6.1
public Tr F<T> (T t, ...) where T: C1, C2, ... {
   /* implementing code using T */
}
C1, C2, ... are constraints that place requirements on the generic parameter T, as shown below. Tr is a return type that may be a generic parameter.
If the body of F invokes methods on T that are not required by specified traits then compilation may fail. We say that constraits are bounds on T because they narrow the range of valid generic parameters.
C# Constraints are declarations that require a generic parameter to satisfy some set of conditions:
where T: BC // derive from or equal to class BC
where T: IF // implement interface IF
where T: class // must be a reference type
where T: struct // must be value type
where T: new() // can be created with new()
...
Common interfaces include: IEnumerable<T>, IComparable<T>, IEquatable<T>, ICollection<T>, ...

Generic classes are defined with the syntax:
public class SomeType<T>
    where T: C1, C2, ...
{
    SomeType(T arg1, ...) {
        /* code to initialize instance */
    }
    /* code declaring additional SomeType methods and data */
}

2.0 Source Code

Examples below show how to use library and user defined types with emphasis on illustrating syntax and basic operations. These demonstrations of generic C# code are partitioned into modules Program.cs, Stats.cs, Points.cs, and Analysis.cs. Discussion of each of these will be presented in separate sections of the page, below, accessed from links in the left panel.

2.1 Standard Generic Library Types

Standard generic types are defined in the .Net Standard Library and include many more than shown here. We have selected three frequently used types for demonstration: arrays, lists, and dictionaries. You can find details of the others from the references. The examples below show how to use library and user defined types with emphasis on illustrating syntax and basic operations. Alter splitter-bar panel widths by dragging splitter bar or clicking in either panel to expand that panel's width. Default widths are set by setting width style on left panel.
static void Demonstrate_Std_Library_Types() {

  Display.ShowNote("Demonstrate std library generic types", nl);

  Display.ShowOp("String[] array processed by generic function");
  String[] array = {"one", "two", "three"};
  Display.ShowTypeEnum(array, "array", 5, nl);

  Display.ShowOp("List<double>");
  List<double> aList =
  new List<double>{ 1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0 };
  Display.ShowTypeEnum(aList, "aList", 5, nl);

  Display.ShowOp("Dictionary<int, String>");
  var d1 = new Dictionary<int, String>
  {
    { 0, "zero"}, {1, "one"}, {2, "two"}
  };
  Display.ShowTypeEnum(d1, "d1", 5, nl);

}
--------------------------------------------------
Demonstrate std library generic types
--------------------------------------------------

--- String[] array processed by generic function ---
array: String[], size: 8, reference type
value:
  array {
    one, two, three
  }

--- List ---
aList: List, size: 8, reference type
value:
  aList {
    1, 1.5, 2, 2.5, 3,
    3.5, 4
  }

--- Dictionary ---
d1: Dictionary, size: 8, reference type
value:
  d1 {
    [0, zero], [1, one], [2, two]
  }

1.2 User-defined Custom Types

C# defines custom types with the "class" keyword. Classes are patterns for laying out instances in memory and define operations for the instance data. Generic classes use one or more generic parameters as placeholders for specific defined types. That supports providing a single design that works for multiple types, improving code abstraction and developer productivity. Hello<T> is a simple demonstration of how to create and use C# generic classes.

1.2.1 Hello<T> Definition

public class Hello<T> where T:new() {
  public Hello() {
    datum = new T();
  }
  public Hello(T t) {
    datum = t;
  }
  public void show(String nm) {
    Console.Write("  {0}: {{ {1} }}\n", nm, datum);
  }
  public T datum { get; set; }  // property
}


Hello<T>:

class Hello<T> declares a generic parameter T that
is a placeholder of specific concrete types, e.g., int or
float. One definition provides an open-ended number
of concrete classes, one for each concrete type declared in
program code.
The generic parameter is required to provide a constructor
with the constraint where T : new().
This class defines a property, datum that exposes a value
with get and set operations and two overloaded constructors,
one that initializes the property value to a default value
and one that initializes the property to a specified value.
It also defines a function show(string nm) that displays
a representation of the instance on the terminal.
This is a simple example designed to illustrate syntax for
C# classes. More details follow in following code blocks.

1.2.2 Hello<T> Demonstration

Display.ShowOp("Hello<int>");
Hello<int> hi = new Hello<int>(42);
Display.ShowType(hi, "hi");
Console.WriteLine("value:");
hi.show("h");
Display.ShowOp("Hello<double>");
Hello<double> hd = new Hello<double>(3.1415927);
Console.WriteLine("access hd.datum: {0}", hd.datum);
hd.show("hd");
Console.WriteLine();
--- Hello<int> ---
hi: Hello<T>, size: 8, reference type
value:
  h: { 42 }
--- Hello<double> ---
access hd.datum: 3.1415927
  hd: { 3.1415927 }

1.2.3 Stats<T> Definition

Stats.cs is a single file that defines the Stats<T> type. All of the code for this file is shown in the left panel below. It illustrates in a simple way how useful computational functionality can be provided for consumption by application programs.
/*-------------------------------------------------------------------
  Stats.cs
  - provides definitions for user-defined class Stats<T>
*/

using System.Collections;
using System.Collections.Generic;   // IEnumerable<T>, List<T>, ...
using System.Numerics;              // INumber<T>
using Analysis;

namespace Stats {
  /*----------------------------------------------------------------------
    Stats<T> is a generic demo type
     - holds any finite number of generic values
     - computes several statistics on its values
  */
  public class Stats<T>
    where T: INumber<T>
  {
    /*--------------------------------------------------------------------
      Constructs a point with N coordinates each with default value
    */
    public List<T> items { get; set; }
    public Stats(List<T> l) {
      items = l;
    }
    public void check() {
      if (items == null) {
        throw new SystemException("stats is null");
      }
      else if (items.Count < 1)
        throw new SystemException("stats is empty");
    }
    public int size() {
      check();
      return items.Count;
    }
    public T max() {
      check();
      T max = items[0];
      foreach(T item in items) {
        if(max.CompareTo(item) < 0)
          max = item;
      }
      return max;
    }
    public T min() {
      check();
      T min = items[0];
      foreach(T item in items) {
        if(min.CompareTo(item) > 0)
          min = item;
      }
      return min;
    }
    public T? sum() {
      check();
      dynamic? sum = default(T);
      if(sum != null) {
        foreach(T item in items) {
          sum += item;
        }
        return sum;
      }
      return default(T);
    }
    public double avg() {
      check();
      dynamic? sum = this.sum();
      return Convert.ToDouble(sum)/Convert.ToDouble(items.Count);
    }
  }
}

Stats<T>:

This demonstration shows how a user-defined
class can implement arithmetic operations on
its data members.
In order for that to work the generic parameter
T has to be constrained to those that support
arithmetic operations. That's achieved with the
bound where T : INumber<T>.
Stats<T> defines its data items as a property
holding a List<T>. That encapsulates the data
inside getter and setter methods that can, if
needed, control access to and modifications of
the data. How to do that is shown here.
INumber<T> constraint
items property
To make the property immutable, simply remove
set;.

1.2.4 Stats<T> Demonstration

Display.ShowOp("Stats<int>");
List<int> tmp1 = new List<int> { 1, 2, 3, 2, 1 };
Stats<int> si = new Stats<int>(tmp1);
ShowList(tmp1);
Console.WriteLine("  max: {0}", si.max());
Console.WriteLine("  min: {0}", si.min());
Console.WriteLine("  sum: {0}", si.sum());
Console.WriteLine("  avg: {0:0.00}", si.avg());
Console.WriteLine();

Display.ShowOp("Stats<double>");
List<double> tmp2 = new List<double> { 1.0, 1.5, 2.0, 1.5, 1.0 };
Stats<double> sd = new Stats<double>(tmp2);
ShowList(tmp2);
Console.WriteLine("  max: {0:0.0}", sd.max());
Console.WriteLine("  min: {0:0.0}", sd.min());
Console.WriteLine("  sum: {0:0.0}", sd.sum());
Console.WriteLine("  avg: {0:0.00}", sd.avg());
Console.WriteLine();
--- Stats<int> ---
  [ 1, 2, 3, 2, 1 ]
  max: 3
  min: 1
  sum: 9
  avg: 1.80

--- Stats<double> ---
  [ 1, 1.5, 2, 1.5, 1 ]
  max: 2.0
  min: 1.0
  sum: 7.0
  avg: 1.40

1.2.5 Point<T> Definition

Point<T> is similar to the generic types Point<T, N> defined for C++ and Rust. There is one fundamental difference; the C++ template and Rust generic point types each take an integer parameter N. The C# generic point class does not. C# does not support adding integer types to a generic parameter list, e.g., <T, N> is not allowed.
/*-------------------------------------------------------------------
  Points.cs
  - provides definitions for user-defined class Point<T>
*/

using System.Collections;
using System.Collections.Generic;   // IEnumerable<T>, List<T>
using Analysis;

namespace Points {
  /*----------------------------------------------------------------------
    Point<T> represents a point in N-dimensional space
     - C# generics do not accept integers as generic parameters
       so we cannot declare P<T,N> where N is an integer like
       we did for C++ and Rust
     - Point holds any finite number of generic coordinates
     - coordinates are held in a List<T>
     - implements IEnumerable<T> so it can be indexed and iterated
     - it is a reference type because it is implemented with a class.
       its List<T> is also a reference type
  */
  public class Point<T> : IEnumerable<T>, Analysis.IShow
  {
    /*--------------------------------------------------------------------
      Constructs an empty point
    */
    public Point() {
      coor = new List<T>();
    }
    /*--------------------------------------------------------------------
      Constructs a point with N coordinates each with default value
    */
    public Point(int N) {
      coor = new List<T>();
      for(int i = 0; i<N; ++i) {
        T? test = default(T);
        if(test != null) {
          coor.Add(test);
        }
      }
    }
    /* translates IShow::show() for needs of Point class */
    public void Show(string name, int _width = 5, int _left = 2) {
      int SaveWidth = Width;
      int SaveLeft = Left;
      Width = _width;
      Left = _left;
      PrintSelf(name);
      Width = SaveWidth;
      Left = SaveLeft;
    }
    /*
      Displays structure and state of N-dimensional point.
      - state is a set of rows of coordinate data
      - property Width specifies number of elements in each row
      - property Left specifies offset of row from terminal Left edge
    */
    public void PrintSelf(string name) {
      Console.Write(Display.Indent(Left));
      Console.Write("{0} {{\n{1}", name, Display.Indent(Left + 2));
      for(int i=0; i<coor.Count; ++i) {
        Console.Write("{0}", coor[i]);
        if(i < coor.Count - 1) {
          Display.print(", ");
        }
        if((i+1) % Width == 0 && i != coor.Count - 1) {
          Console.Write("\n");
          Console.Write(Display.Indent(Left + Indent));
        }
      }
      Console.Write("\n{0}", Display.Indent(Left));
      Console.WriteLine("}");
      String callname = "  " + dt;
      Console.WriteLine(callname);
    }
    /* The three functions below support indexing and iterating */
    public T this[int index] {
      get { return coor[index]; }
      set { coor.Insert(index, value); }
    }
    public IEnumerator<T> GetEnumerator() {
      return coor.GetEnumerator();
    }
    IEnumerator IEnumerable.GetEnumerator() {
      return this.GetEnumerator();
    }
    /* supports inializer list */
    public void Add(T t) {
      coor.Add(t);
    }
    public void AddRange(IEnumerable<T> list) {
      coor.AddRange(list);
    }
    public List<T> coor { get; set; }
    public DateTime dt { get; set; } = DateTime.Now;
    public int Length { get { return coor.Count; } }
    public int Width { get; set; } = 5;   // default row size
    public int Left { get; set; } = 2;    // default offset
    public int Indent { get; set; } = 2;
    /* initializers */
    public Point<T> width(int w) {
      this.Width = w;
      return this;
    }
    public Point<T> left(int l) {
      this.Left = l;
      return this;
    }
    public Point<T> indent(int i) {
      this.Indent = i;
      return this;
    }
  }
}

Point<T>

A reference type parameterized on T, the type of
its coordinate members.
Import Analysis and Display classes.
Point<T> implements
IEnumerable<T> and IShow interfaces.
Compilation will fail if the class does
not implement all of the methods declared
with these interfaces.
Constructor: Point(int n)
Unlike C++ and Rust, C# cannot use integral
types as generic parameters, so the number of
coordinates is passed as a constructor argument.
Initialized Constructor:
Point<int> { 1, 2, 3, 4, 5 }
implicitly uses Add(T t) to add elements
to coor.
Display format:
Width sets number of elements per row
Left sets left margin
Indent sets offset of data within delimiters

1.2.6 Point<T> Demonstration

The panel view below is wrapped with a flex container and the right panel is set to fill space not occupied by left panel. Both left and right panels have white-space:pre style. Text color in the left panel uses syntax highlighting generated by the prism styling from prism.com. This is intended for code display. The right panel is also pre styling with terminal colors, intended for output display.
Display.ShowOp("Point<double>");

Point<double> p = new Point<double>(5);
List<double> tmp = new List<double>{ 1.0, 2.5, 3.0, -2.5, 1.0 };
p.coor = tmp;
Display.ShowTypeEnum(p, "p");
Display.ShowOp("use indexing to retrieve and modify coordinates");
Console.WriteLine("  value of p[1] is {0}", p[1]);
p[1] = -2.5;
Display.ShowOp("using p.show(callName)");
p.Show("p");
Console.WriteLine();

/* uses default constructor with initializer list */
Display.ShowOp("new Point<int> { 1, 2, 3, 4, 5}");
Point<int> pi = new Point<int> { 1, 2, 3, 4, 5 };
pi.Show("pi");
List<int> rng = new List<int> {4,3,2,1};
Display.ShowOp("pi.AddRange(rng)");
pi.AddRange(rng);
int[] arr = { 0, -1, -2 };
pi.AddRange(arr);
pi.width(6);
pi.Show("pi");
Console.WriteLine();
--- Point<double> ---
p: Point<T>, size: 8, reference type
value:
  p {
    1, 2.5, 3, -2.5, 1
  }
--- use indexing to retrieve and modify coordinates ---
  value of p[1] is 2.5
--- using p.show(callName) ---
  p {
    1, -2.5, 2.5, 3, -2.5,
    1
  }
  6/27/2024 2:27:15 PM

--- new Point<int> { 1, 2, 3, 4, 5} ---
  pi {
    1, 2, 3, 4, 5
  }
  6/27/2024 2:27:15 PM
--- pi.AddRange(rng) ---
  pi {
    1, 2, 3, 4, 5,
    4, 3, 2, 1, 0,
    -1, -2
  }
  6/27/2024 2:27:15 PM

1.2.7 Generic Functions Definition

The Analysis library provides 14 methods, of which 9 are generic. They illustrate the power of generics to provide reusable, configurable code.
/*-------------------------------------------------------------------
  Analysis.cs
  - provides several type analysis and display methods in the
    class Display
*/
using System;
using System.Collections;
using System.Collections.Generic;     // IEnumerable<T>, List<T>
using System.Linq;                    // IEnumerable<T>.ToArray
using System.Text;                    // StringBuilder
using System.Reflection;
using System.Reflection.Emit;         // managed size
using System.Runtime.InteropServices; // GCHandle

namespace Analysis {
  /*----------------------------------------------------------
    IShow interface allows analysis functions to operate on
    instances of any class that implements it.
    - See ShowTypeShowable<T>(T t, ...), below.
  */
  public interface IShow {
    void Show(string name, int Width = 5, int Left = 2);   // show instance state as rows of elements
    int Length { get; }       // total number of elements
    int Width { get; set; }   // number of elements per row
    int Left { get; set; }    // offset from terminal Left
    int Indent {get; set; }   // element indent from braces
  }
  /*-----------------------------------------------------------------------
    Collection of static functions that present information about types
    and data values on the terminal line.
  */
  public class Display {
    public static String Spaces(int i) {
      return new String(' ', i);
    }
    public static bool IsAssociativeColl(IEnumerable coll) {
      return coll is IDictionary;
    }
    /*-------------------------------------------------
      Show static type with some formatting adjustments
      and name at callsite.
    */
    public static void ShowType<T>(T t, string nm) {
      Type tt = t!.GetType();
      string tnm = tt.Name;
      if(tt.IsGenericType) {
        if(tnm.Length > 1) {
          tnm = tnm.Remove(tnm.Length - 2) + "<T>";
        }
      }
      Console.Write("{0}: {1}, ", nm, tnm);
      int size = GetManagedSize(tt);
      Console.Write("size: {0}, ", size);
      if (tt.IsValueType) {
        Console.WriteLine("value type");
      }
      else {
        Console.WriteLine("reference type");
      }
    }
    /*-------------------------------------------------
      Display information about the type of any scalar type.
      - scalar types are those with a single value like:
          int, double, string, ...
      - This function directly uses only simple reflection
    */
    public static void ShowTypeScalar<T>(
      T t, string nm, string suffix = ""
    )
    {
      ShowType(t, nm);
      Console.WriteLine("value: \"{0}\"{1}", t, suffix);
    }
    /*-------------------------------------------------
      Show type information for any type that implements
      IEnumerable<T> interface.
    */
    public static void ShowTypeEnum<T> (
    IEnumerable<T> t, string nm, int w = 5, string suffix = ""
    )
    {
      Type tt = t!.GetType();
      string tnm = tt.Name;
      if(tt.IsGenericType) {
        if(tnm.Length > 1) {
          tnm = tnm.Remove(tnm.Length - 2);
          if(IsAssociativeColl(t)) {
            tnm += "<K,V>";
          }
          else {
            tnm += "<T>";
          }
        }
      }
      Console.Write("{0}: {1}, ", nm, tnm);
      int size = GetManagedSize(tt);
      Console.Write("size: {0}, ", size);
      if (tt.IsValueType) {
        Console.WriteLine("value type");
      }
      else {
        Console.WriteLine("reference type");
      }
      Console.WriteLine("value:\n{0}{1} {{", "  ", nm);
      /*
        beautify value list into rows of w elements
        indented by 4 spaces
      */
      string tmp = FoldArray(t.ToArray(), w, 4);
      Console.Write(tmp);
      Console.WriteLine("\n  }");
      Console.Write(suffix);
    }
    /*-------------------------------------------------
      Show type information for any type that implements
      the IShow interface
    */
    public static void ShowTypeShowable<T>(
      T t, string nm, string suffix=""
    )
    where T:IShow
    {
      ShowType(t, nm);
      Console.WriteLine("value:");
      t.Show(nm);     // guaranteed by IShow implementation
      Console.Write(suffix);
    }
    /*-------------------------------------------------
      Provides name of caller, nm, as label for IShow() information.
      - works for all Showable instances
    */
    public static void ShowLabeledObject<T>(
      T t, string nm
    ) where T:IShow {
      // Console.Write(nm);
      t.Show(nm);
    }
    /*-------------------------------------------------
      Provides name of caller, nm, as label for value.
      - works for all T with ToString.
    */
    public static void DisplayLabeledObject<T>(T t, string nm) {
      Console.WriteLine("{0}: {1}", nm, t!.ToString());
    }
    /*-------------------------------------------------
      create string of count spaces, used to offset output
    */
    public static string Indent(int count) {
      StringBuilder sb = new StringBuilder();
      sb.Append(' ', count);
      return sb.ToString();
    }
    /*-------------------------------------------------
      Truncate string to length of N, but only if
      its length is greater than N
    */
    public static string truncate(int N, string bigStr) {
      if(bigStr.Length <= N) {
        return bigStr;
      }
      StringBuilder sb =  new StringBuilder();
      sb.Append(bigStr);
      sb.Length = N;  // move back pointer to desired length
      return sb.ToString();
    }
    /*-------------------------------------------------
      fold array elements into rows of w elements
    */
    public static string FoldArray<T>(T[] arr, int w, int Left) {
      StringBuilder tmp = new StringBuilder();
      tmp.Append(Indent(Left));
      for(int i=0; i< arr.Length; ++i) {
        tmp.Append(arr[i]!.ToString());
        tmp.Append(", ");
        if((i+1) % w == 0 && i != arr.Length - 1) {
          tmp.Append("\n");
          tmp.Append(Indent(Left));
        }
      }
      if(tmp.Length > 1) {
        tmp.Length -= 2;  // don't return last comma and space
      }
      return tmp.ToString();
    }
    /*-------------------------------------------------
      do t1 and t2 share the same address?
    */
    public static void IsSameObj<T>(
      T t1, String n1, T t2, String n2, string suffix = ""
    ) {
      if(ReferenceEquals(t1, t2)) {
        Console.WriteLine(
          "{0} is same object as {1}{2}", n1, n2, suffix
        );
    }
      else {
        Console.WriteLine(
        "{0} is not same object as {1}{2}", n1, n2, suffix);
      }
    }
    /*-------------------------------------------------
      Beware, two distinct objects may have same hashcode.
      - Not used in this demo for that reason.
    */
    public static void showIdent<T>(
      T t, String n, string suffix = ""
    ) {
      int id = t!.GetHashCode();
      Console.WriteLine("{0}, {1}{2}", n, id, suffix);
    }
    /*-------------------------------------------------
      Display function call or operation to help turn
      output data into information
    */
    public static void ShowOp(string op, string suffix = "") {
      Console.WriteLine("--- {0} ---{1}", op, suffix);
    }
    public static void print(String s = "") {
      Console.Write(s);
    }
    public static void println(String s = "") {
      Console.WriteLine(s);
    }
    public static void Println(String s) {
      Console.WriteLine(s);
    }
    /*-------------------------------------------------
      Emphasize text with borders
    */
    public static void ShowNote(string s, int n=60, string suffix = "") {
      string line = new string('-', n);
      Console.WriteLine(
        "--------------------------------------------------"
      );
      Console.WriteLine("{0}", s);
      Console.WriteLine(
        "--------------------------------------------------{0}",
        suffix
      );
    }
    /*-------------------------------------------------
      Surround note with empty lines
    */
    public static void ShowLabel(string s) {
      Console.WriteLine();
      ShowNote(s);
      Console.WriteLine();
    }
  }
}

Generic Methods:

C# does not support free functions. Instead,
methods are defined in a class.
However, static methods of classes play the
same role as free functions in other languages.
interface IShow:
Allows function:
ShowTypeShowable<T>(T t, ...)
to accept any type T that implements IShow.
Generic functions provide abstraction over
types that improves developer productivity,
code readability and maintainability.
ShowTypeEnum<T>(IEnumerable<T> t, ...)
Can display any IEnumerable<T> type, e.g.
Array<T>, List<T>, Point<T>.
ShowTypeShowable(T t, ...)

1.2.8 Complex Generic Methods

The dropdown below provides code for interesting generic methods, some of which use advanced techniques. They are useful know, but none are used in this demonstration.
Complex Methods
/*-----------------------------------------------------------
  All methods below this are not part of this presentation
  - may use advanced code techniques
  - may be experimental
  - may simply be alternate designs that could be
    useful in other applications
-----------------------------------------------------------*/
/*
  Show fields and methods for either reference or value types
  using reflection
*/
// https://stackoverflow.com/questions/7613782/iterating-through-struct-members
public static void iterate<T>(T t) /*where T:new()*/ {
  Console.WriteLine("fields:");
  foreach(
    var field in typeof(T).GetFields(
      BindingFlags.Instance |
      BindingFlags.NonPublic |
      BindingFlags.Public
    )
  ) {
    Console.WriteLine(
      "  {0} = {1}", field.Name, field.GetValue(t)
    );
  }
  Console.WriteLine("methods:");
  foreach(
    var method in typeof(T).GetMethods(
      BindingFlags.Instance | BindingFlags.Public
    )
  ) {
    Console.WriteLine(
      "  {0}", method.Name
    );
  }
}
/*-----------------------------------------------------
Build string representation of array of type T
-----------------------------------------------------*/
public static string ToStringRepArray<T>(T[] arr) {
  StringBuilder sb = new StringBuilder();
  sb.Append("{ ");
  bool first = true;
  foreach(T item in arr) {
    if(item == null) {
      break;
    }
    if(first) {
      sb.Append(item.ToString());
      first = false;
    }
    else {
      sb.AppendFormat(", {0}", item);
    }
  }
  sb.Append(" }\n");
  return sb.ToString();
}
/*-----------------------------------------------------
Build string representation of IEnumerable
collection T<U>. Works for array too.
-----------------------------------------------------*/
public static string ToStringRepIEnumerable<T,U>(T enu)
  where T:IEnumerable<U>
{
  StringBuilder sb = new StringBuilder();
  sb.Append("[ ");
  bool first = true;
  foreach(U item in enu) {
    if(item == null) {
      break;
    }
    if(first) {
      sb.Append(item.ToString());
      first = false;
    }
    else {
      sb.AppendFormat(", {0}", item);
    }
  }
  sb.Append(" ]\n");
  return sb.ToString();
}
/*-----------------------------------------------------
Direct implementation of enumerating associative
collection.  This can also be done with
ToStringRepIEnumerable<Dict,KVPair>(dict).
-----------------------------------------------------*/
public static string ToStringRepAssocCont<Dict,Key,Value>(Dict assoc)
  where Dict:IDictionary<Key,Value>
{
  StringBuilder sb = new StringBuilder();
  sb.Append("{ ");
  bool first = true;
  foreach(var item in assoc) {
    if(first) {
      var sf = String.Format("{{{0}, {1}}}", item.Key, item.Value);
      sb.Append(sf);
      first = false;
    }
    else {
      sb.AppendFormat(", {{{0}, {1}}}", item.Key, item.Value);
    }
  }
  sb.Append(" }\n");
  return sb.ToString();
}
/*-- move this to functions --*/
static void DemoPassValAndRef() {
  Display.ShowNote("Pass by value", 60, "\n");
  double d = 3.1415927;
  Pass_by_value<double>(d, "d");
  TestForNullValue(d, "d");

  List<int> li = new List<int>{ 1, 2, 3, 2, 1 };
  Pass_by_value<List<int>>(li, "li");
  TestForNullValue(li, "li");
}
public static string GetTypeString<T>(T t, String nm, String suffix = "")
{
  /*-- t! asserts that t is not null --*/
  Type tt = t!.GetType();
  string typeInfo = String.Format("{0}: Type: {1}\n", nm, tt.Name);
  int size = GetManagedSize(tt);
  string instanceInfo = String.Format("value: {0}\nsize: {1}{2}", t, size, suffix);
  return typeInfo + instanceInfo;
}
static unsafe void Pass_by_value<T>(T? t, string nm) {
  string ts = GetTypeString(t, nm);
  Console.WriteLine(ts);
  /*
    Suppresses warning about taking address of managed type.
    The pointer is used only to show the address of ptr
    as part of analysis of copy operations.
  */
  #pragma warning disable 8500
  string addrd = ToStringAddress<T>(&t);
  #pragma warning restore 8500
  Console.WriteLine("{0}: {1}", nm, addrd);
  t = default(T);
  /*
    caller sees this change if and only if T is a reference type
    in which case t is null.
  */
}
#pragma warning disable 8500
public static unsafe string ToStringAddress<T>(T* ptr) {
  if(ptr == null) {
    return "";
  }
  IntPtr addr = (IntPtr)ptr;
  string addrstr = string.Format("address: 0x" + addr.ToString("x"));
  return addrstr;
}
#pragma warning restore 8500
/*-- extract address of reference instance in managed heap --*/
public static string ToStringAddressFromHandle<T>(T t) {
  string addrstr = "for handle\n";
  try {
    GCHandle handle = GCHandle.Alloc(t, GCHandleType.Pinned);
    IntPtr address = handle.AddrOfPinnedObject();
    addrstr = "address: " + String.Format("0x" + address.ToString("x"));
    handle.Free();
    return addrstr + "\n";
  }
  catch {
    Console.WriteLine("GCHandle exception thrown");
  }
  return addrstr;
}
static void TestForNullValue<T>(T? t, string nm) {
  if(t == null) {
    Console.WriteLine(nm + " is null");
  }
  else {
    Console.WriteLine(nm + " is {0}", t);
  }
}
static void DemoPrimitives() {
  Display.ShowNote(
    "Examples of creation and display of Primitive Types",
    60
  );
  short s = 123;
  Display.ShowTypeScalar(s, "s", "\n");
  long l = 12345;
  Display.ShowTypeScalar(l, "l", "\n");
  float f = 3.1415927f;
  Display.ShowTypeScalar(f, "f", "\n");
  double d = 3.1415927;
  Display.ShowTypeScalar(d, "d", "\n");
  int[] arr = new int[]{ 4, 3, 2, 1, 0, -1};
  Display.ShowTypeScalar(arr, "arr");
}
/*-------------------------------------------------
  Build string of comma separated values from
  Enumerable collection
  - no longer used here, but will be useful so kept
*/
// https://stackoverflow.com/questions/330493/join-collection-of-objects-into-comma-separated-string
public static string ToCSV<T>(IEnumerable<T> coll) {
  StringBuilder sb = new StringBuilder();
  foreach(T elem in coll) {
    sb.Append(elem!.ToString()).Append(", ");
  }
  return sb.ToString(0, sb.Length - 2);
}
/*-------------------------------------------------
  Returns value of T for IEnumerable<T> at runtime.
  Needed for some functions that operate on generic
  collections.
  - at this time, not used in this demo
*/
// https://www.codeproject.com/Tips/5267157/How-To-Get-A-Collection-Element-Type-Using-Reflect
public static Type? GetTypeOfCollection(Object coll) {
  Type type = (coll).GetType();
  var etype = typeof(IEnumerable<>);
  foreach (var bt in type.GetInterfaces()) {
    if (bt.IsGenericType && bt.GetGenericTypeDefinition() == etype) {
      return (bt.GetGenericArguments()[0]);
    }
  }
  return null;
}
/*----------------------------------------------------------------
  This method uses advanced relection

  - GetMangedSize(Type type) is function that returns the size of
  value types and handles, used to help discover how things work.
  It is placed here because it uses advanced techniques that
  will eventually be covered elsewhere in this site.  Knowing
  how it works is not essential for the things we are examining
  in this demo.
*/
// https://stackoverflow.com/questions/8173239/c-getting-size-of-a-value-type-variable-at-runtime/8173293#8173293
public static int GetManagedSize(Type type)
{
  // all this just to invoke one opcode with no arguments!
  var method = new DynamicMethod(
    "GetManagedSizeImpl", typeof(uint), new Type[0],
    typeof(TypeExtensions), false
  );

  ILGenerator gen = method.GetILGenerator();
  gen.Emit(OpCodes.Sizeof, type);
  gen.Emit(OpCodes.Ret);

  var func = (Func<uint>)method.CreateDelegate(typeof(Func<uint>));
  return checked((int)func());
}
These functions provide additional functionality
that will be useful in other programs, but are
not used in this demonstration.
Some of them use advanced reflection tech-
niques or other designs that are interesting.

1.3 Program Structure

The code below, Program.cs, imports libraries: Points, Analysis, and Stats. Using those facilities it orchestrates all the demonstrations shown here by calling in Main three methods developed above Main. Details of those methods have been elided to show the structure of this program. All of the elided code has already been shown in the code blocks above.
/*-------------------------------------------------------------------
  Cs_Generic::Program.cs
  - demonstrates creating and using std library generic types:
      array, List<T>, Dictionary<K,V>
  - demonstrates creating and using user-defined generic types:
      Demo<T>, Stats<T>, and Point<T, N>
  - depends on PointsGeneric.cs to provide user-defined Point class
  - depends on Stats.cs to provide user-defined Stats class
  - depends on AnalysisGeneric.cs for several display and analysis
    functions
*/
using System;
using System.Collections;
using System.Collections.Generic;   // IEnumerable<T>, List<T>
using System.Linq;                  // IEnumerable<T>.ToArray
using System.Text;                  // StringBuilder
using Points;                       // defined in Points.cs
using Analysis;                     // defined in Analysis.cs
using Stats;                        // defined in Stats.cs
/*-----------------------------------------------
Note:
Find all Bits code, including this in
https://github.com/JimFawcett/Bits
You can clone the repo from this link.
-----------------------------------------------*/
namespace CSharpGenerics
{
  /* definition of Hello<T> code elided */

  /*-- Generic demonstration begins here --*/

  class Program
  {
    const string nl = "\n";

    static void ShowList<T>(List<T> lst) {
      /* code elided */
    }

    static void Demonstrate_Std_Library_Types() {
      /* code elided */
    }
    static void Demonstrate_User_defined_Types() {
      /* code elided */
    }
    static void Demostrate_generic_functions() {
      /* code elided */
    }
    static void Main(string[] args)
    {
      Display.ShowLabel(" Demonstrate C# generics");

      Demonstrate_Std_Library_Types();
      Demonstrate_User_defined_Types();
      Demostrate_generic_functions();

      Console.WriteLine("\nThat's all Folks!\n");
    }
  }
}

Program

This code block shows how Program.cs imple-
ments all of the demonstrations in this
Generic C# Bit.
Code details, shown elsewhere, have been
elided to show clearly the structure of
this demonstration.

3.0 Build


C:\github\JimFawcett\Bits\CSharp\Cs_Generic
> dotnet build
MSBuild version 17.5.1+f6fdcf537 for .NET
  Determining projects to restore...
  All projects are up-to-date for restore.
  Cs_Objects -> C:\github\JimFawcett\Bits\CSharp\Cs_Generic\bin\Debug\net7.0\Cs_Ob  
  jects.dll

Build succeeded.
    0 Warning(s)
    0 Error(s)

Time Elapsed 00:00:01.79
C:\github\JimFawcett\Bits\CSharp\Cs_Generic
          

4.0 VS Code View

The code for this demo is available in github.com/JimFawcett/Bits. If you click on the Code dropdown you can clone the repository of all code for these demos to your local drive. Then, it is easy to bring up any example, in any of the languages, in VS Code. Here, we do that for CSharp\Cs_Generic. Figure 1. VS Code IDE - C# Generics

4.0 References

Reference Description
C# Generics - tutorialspoint Basic syntax with examples
C# Tutorial - w3schools Interactive examples
C# Reference - Microsoft Relatively complete informal language reference