about
Bits: C# Data
11/25/2023
0
Bits Repo Code Bits Repo Docs

Bits: C# Data Types

types, initialization, construction, assignment

Synopsis:

Most of a language's syntax and semantics derives directly from its type system design. This is true of all of the languages discussed in these Bits: C++, Rust, C#, Python, and JavaScript.
C# has a rich complex type system, divided into two fundamentally different parts: value types and reference types. Its value types are similar to the primitive types defined by C++ and Rust. Reference types each consist of a handle in stack memory referring to a value stored in the dotnet managed heap. Execution is controlled by a virtual machine called the Common Language Runtime (CLR).
C# was created starting from Microsoft's Java definition, with This page demonstrates simple uses of the most important C# types. The purpose is to quickly acquire some familiarity with types and their uses.
  • Value types and aggregates of value types are copyable. Assignment and pass-by-value copies the source's value to the destination.
  • Reference types are not copyable. Assignment and pass-by-value copies the reference, not the referenced instance. That results in two handles referring to the same heap-based instance.
  • Value types have a nullable variant declared by adding a "?" suffix, e.g., bool? b;. Nullable value types hold null by default and may be assigned a null value as well as non-null values. The value can be tested with Nullable<T>.HasValue property which holds a boolean, true if tεT is non-null, otherwise false.
  • Reference types may be decorated with a "?" to indicate that it is a nullable type. They may also be decorated with the "!" suffix to assert that they are not null. That is done when the compiler's static analysis yields a "maybe null" state for the variable but the designer knows that it is not null.
  • C# does not support move operations since the CLR owns all reference type instances. There is no need to use moves to avoid the expense of possibly large copies, since the language does not directly support making copies of reference types.
  • Value types may be stored in stack or static memory. Reference types may only be stored in the program's managed heap.
  • C# is memory safe because all memory management operations are executed by the CLR, not by user's code. That comes with performance penalties for throughput and latency that are significantly worse than that of native code languages like C++ and Rust.
  • Here we begin to see significant differences between the languages, especially when comparing statically typed languages like C++, Rust, and C#, with dynamically typed languages like Python and JavaScript.
C# Types Details  Types are defined by the size of their memory allocations, the encodings used to place values in memory, and rules that define how the values can be accessed, modified, and combined. The name of an instance of a Value type binds to a single contiguous block of stack memory. Each name is an identifier to a unique block of memory. The name of an instance of a Reference type binds to a unique handle that refers to a heap allocation that may have other handles referring to it.

Table 1.0 C# Types

Type Comments Example
-- Integral value types ----
bool values true and false bool b = true;
byte unsigned 8-bit integer byte bt = 0x2A;
sbyte signed 8-bit integer byte sb = 0xffD6;
int
with short, ushort, uint, long, ulong qualifiers
int is a signed 32 bit integer int i = 42;
nint, unint nint and unint are signed and unsigned native size integers. Size is platform dependent. nint i = 42;
char 16 bit unsigned value char c = 'a';
-- Floating point value types ----
float size = 4 bytes, values have finite precision, and may have approximate values float f = 3.1415927;
double size = 8 bytes, values have finite precision, and may have approximate values double d = 3.14159265359;
decimal size = 16 bytes, values are exact, has no exponent decimal dc = 100000000.00;
-- Aggregate types ----
T[] Reference type, array of N elements, all of type T int[]arr = { 1, 2, 3, 2, 1 };
int first = arr[0];
(T1, T2, ...) Tuple value type, collection of heterogeneous types accessed by position (int, float, char)tup = (42, 3.14159, 'z');
char third = tup.Item3;
struct Value type, collection of heterogeneous types accessed by name struct S { public int I; public double D; }
S s; s.I = 42; s.D = 3.14159;
int first = s.I;
-- Reference types defined by C# language ----
object Base for all value and reference types
with methods GetType, ToString, Equals, ...
object o = new object();
string type = o.GetType().ToString();
string Expandable collection of Unicode characters allocated in the managed heap string str = new("a string"); Console.WriteLine("str: {0}", str);
delegate Reference type that encapsulates a method delegate string methodDelegate(int i, string s, ...);
dynamic An instance of dynamic type can be bound to any type of data. Use of a dynamic instance is checked at run-time, not compile-time. dynamic dyn = 42;
Console.WriteLine("dyn: {0}", dyn);
dyn = 3.1415927;
Console.WriteLine("dyn: {0}", dyn);
class class is a meta-type, used to define library and user reference types. public class X {
  public X(int ia, double da) {
    i = ia;
    d = da;
  }
  public int i {get;set;} = 0;
  public double d {get;set;} = 0.0;
}
X x = new X(42, 3.1415927); Console.WriteLine("X.d: {0}", x.d);
-- Reference types defined by .net library ----
List<T> Expandable list of elements of type T List<int> li =
  new List<int> { 1, 2, 3, 2, 1 };
int lfirst = li[0];
Dictionary<K, V> Unordered associative container of Key-Value pairs, held in table of buckets. Dictionary<string, int> dict =
  new Dictionary<string, int>()
dict["zero"] = 0;
Many additional types defined by library Types for reading and writing, parallel and concurrent operations, synchronization, ...
-- User-defined Types --
User-defined types Based on classes and structs, these will be discussed in the next Bit.
C# Type System Attributes 

Table 2. C# Type System Attributes

Static typing All non-dynamic types are known at compile-time and are fixed throughout program execution.
Dynamic typing Dynamic types are evaluated at run-time and may be bound to any type of data.
Type inference Compiler infers types in expressions if not explicitly annotated or if declared var. Occasionally inference fails and explicit annotation is required.
Intermediate strength typing Types are exhaustively checked but allow both implicit and explicit conversions.
  • Numeric and boolean literals coerce to their correspoinding type, e.g., 42 to int.
  • Variables with var type declarations are coerced to the type of their RHS's
  • Values can be coerced using user-defined explicit and implicit conversion methods.
Generics Generics provide types and functions with unspecified parameters, supporting code reuse and abstraction over types. Generic parameters are specified at the call site, e.g., doOp<T, U>(T t, U u). The function doOp is checked for syntax before instantiating with specific type(s). Calling methods on t or u in the doOp body require type constraints to interfaces that declare those methods. That will be demonstrated in the Bit on Generics.

1.0 Initialization

Initialization is the process of endowing a newly created type instance with a specified value. Uninitialized local value type instances have undefined values. All other uninitialized variables are compiler-initialized to null for reference types, zero for numeric types, and false for bools. To insure well defined behavior initialize all varibles where they are declared, as shown below.

1.1 Value Types

Instances of value types each occupy one block of contiguous memory. Copy construction and assignment results in source and destination being two unique instances. After those operations, change to the destination does not affect the source. The semantics of copy construction and assignment are different for reference types. Several of the code blocks shown below have formatting and output code elided. You can find complete code in the Bits Repository:   Program.cs, AnalysisData.cs,
  /*---------------------------------
    All code used for output has 
    been elided
  */
  /*-- bool --*/
  bool b = true;

  /*-- int --*/
  int i = 42;

  /*-- char --*/
  char c = 'z';

  /*-- double --*/
  double d = 3.1415927;

  /*-- decimal --*/
  decimal dec = 100_000_000.00m;
These scalars, types with a single value, are initialized by assigning a value. The int type can be qualified with keywords short, ushort, uint, long, and ulong. Floating point types are float, double, and decimal. A complete list of types and their qualifiers are given in the "C# Types Details" dropdown list, above.
Output
  -------------------------
  Value Types
  -------------------------

  --- bool b = true; ---
  b: Type: Boolean
  value: True
  size: 1

  --- int i = 42 ---
  i: Type: Int32
  value: 42
  size: 4

  --- char c = 'z' ---
  c: Type: Char
  value: z
  size: 2

  --- double d = 3.1415927; ---
  d: Type: Double
  value: 3.1415927
  size: 8

  --- decimal dc = 100_000_000.00m; ---
  dec: Type: Decimal
  value: 100000000.00
  size: 16

Each type in the code block on the left is characterized by its value, its type evaluated using reflection with
   Type tt = t.GetType()
and its size retrieved using reflection in the function
   int GetManagedSize(Type type)
That is defined in the AnalysisData.cs file, and shown in Section 3.0, below.
Value types are held in a single block of contiguous memory. Value copies are byte-by-byte copies of value from source location to the destination. The char type has size of two bytes, e.g., 16 bits. That holds a Unicode UTF-16 character. The string type holds an array of these chars in the managed heap. The decimal type is larger than double. Decimals are intended to hold large numbers with no exponent. The double type has lower precision, but can approximate very large numbers using an exponent. Its contiguous memory allocation is partitioned into a value part and exponent part.

1.2 Language-defined Aggregate Types

Aggregates are types composed of a collection of zero or more instances of item type(s). The language-defined aggregates are arrays, T[], tuples, (T1, T2, ...), and structs, struct { T1, T2, ... }.
  /*-- array --*/
  int[] array = { 1, 2, 3, 2, 1 };
  int first = array[0];

  /*-- tuple --*/
  (int, double, char)tup = (42, 3.14159, 'z');
  double second = tup.Item2;

  /*-- struct --*/
  S s = new S(42, 3.1415927);
  int sfirst = s.I;

Arrays, T[], consist of a sequence of elements all of the same type. They are initialized with a braced list of values, and each element is accessed with array indexes. Tuples, (T1, T2, ...), are a heterogeneous collection of instances of arbitrary types. Their elements are initialized with assignment and accessed by position. Structs, struct { T1, T2, ... } are also a collection of heterogeneous types, but individual elements are accessed by name. They are initialized with a constructor, e.g., an initializing function with the name of the struct.
Output
  --- int[]array = { 1, 2, 3, 2, 1} ---
  array: Type: Int32[]
  value: System.Int32[]
  size: 8
  { 1, 2, 3, 2, 1 }

  --- (int, double, char)tup = (42, 3.14159, 'z'); ---
  tup: Type: ValueTuple`3
  value: (42, 3.14159, z)
  size: 16

  --- S s = new S(42, 3.1415927); ---
  s: Type: S
  value: CSharpData.Program+S
  size: 16
  S { 42, 3.1415927 }

Arrays are reference types, so the size shown here is the size of a handle to the array in the .net managed heap. Tuples are value types. The size shown is for storage of all the values, e.g., 4 bytes for the integer, 8 bytes for the double, and 2 bytes for the char, with an additional 2 bytes for padding, used to optimize read/write times. Structs are also value types. The size shown is for storage of all the values, e.g., 4 bytes for the integer and 8 bytes for the double, with 4 bytes of padding.

1.3 Language-defined Reference Types

Instances of reference types are stored in C#'s managed heap. A reference to that storage is held by the declaring code. Copy construction and assignment results in source and destination references being copied, but the source instance's data is not copied. After those operations, both source and destination hold references to the same instance of data in the managed heap. Change to the destination affects the source. There are many types associated with C# defined by the language and by its libraries. The types presented in this block are defined directly by the language, e.g., object, string, dynamic, and class.
  /*-- object --*/
  object o = new object();

  /*-- string --*/
  string str = "a string";
  string str_alt = new("another string");

  /*-- dynamic --*/
  dynamic dyn = 42;
  dyn = 3.1415927;

  /*-- class --*/
  X x = new X(42, 3.1415927);
  int xFirst = x.i;
            
Object is the base type for the C# reference types, providing reflection capabilities and a few other base facilities, i.e., non-generic collections are defined to hold objects, allowing instances of other types to be bound, as elements, to a collection instance. All other reference types derive directly, or indirectly, from the object class. String represents a constant sequence of characters. C# strings have no null terminator, unlike C and C++. Strings cannot be directly modified, but changes result in a new string instance with copies of the original charcter sequence with the new modification. The type dynamic is a third type category, e.g., values, references, and dynamic types. Instances of dynamic types can be bound to new values of any type at run-time, like Python and JavaScript types. All validity checking happens at run-time. Class is a meta-type, used to create user-defined types. Most C# programs use both user-defined types and library types specified by classes. This example uses a demonstration type X defined as:
   class X { int i { get; set; } = 0; double d { get; set; } = 0.0; }.
The { get; set; } syntax represents a property endowed with getter and setter functions. Properties have the same use syntax as public data, but provide encapsulation of their values.
Output
  --- object o = new object(); ---
  o: Type: Object
  value: System.Object
  size: 8

  --- string str = "a string" ---
  str: Type: String
  value: a string
  size: 8

  --- dynamic dyn = 42; ---
  dyn: Type: Int32
  value: 42
  size: 4
  dyn: Type: Double
  value: 3.1415927
  size: 8

  --- X x = new X(42, 3.1415927); ---
  x: Type: X
  value: CSharpData.Program+X
  size: 8
  X { 42, 3.1415927 }
            
Note that all sizes are the same, i.e., 8 bytes. That's because the specified types are really handles to instances of those types stored in the managed heap. Copies of these are copies of the handles, not copies of the underlying type instances. Consequently, the result of a copy is two handles pointing to the same instance in the managed heap. The string type represents a constant sequence of unicode (16 bit) characters. Modification creates a new string with specified modification to the original characters. This is referred to as "copy on write". Instances of the dynamic type are frequently used to interoperate with foreign code, especially native C++ used to define Microsoft Component Object Model (COM) facilities. In that case, dynamic typing greatly simplifies boilerplate code needed to use COM objects. Class defines a code structure that binds together functions and associated data, all focused on one particular concept or transformation, e.g., data records, events, messages, ... The .net libraries define many more types; and user programs define application specific types, that frequently use these language defined types as bases or aggregated instances.

1.4 Library-defined Reference Types

Many types are defined in System.Collections and System.Collections.Generic. Typical examples are the sequential collections: Array, ArrayList, List<T>, Queue, Stack, which all implement the IList inteface. The associative collections: Hashtable, SortedList, SortedList<Tkey, TValue>, Dictionaryt<Tkey, TValue> all derive from the IDictionary interface. We focus here on the Commonly Used Collections List<T> and Dictionary<TKey, TValue> types.
  /*-- List<int> --*/
  List<int> li = new List<int> { 1, 2, 3, 2, 1 };
  int lfirst = li[0];
  li.Insert(5, 0);

  /*----------------------------------------------
    Alias declaration, shown here, must immediately
    follow a namespace declaration (see top of file)
    - using Dict = Dictionary<string,int>;
    This alias is used to simplify Dictionary 
    declarations below.
  */
  Dict dict = new Dict();
  dict.Add("three", 3);
  dict["zero"] = 0;
  dict["one"] = 1;
  dict["two"] = 22;
  dict["two"] = 2;  // overwrites previous value
  int oneval = dict["one"];
  /*-----------------------------------------
    Find first key and value
    - this is here just to show how to retrieve
      an element from an associative collection
  */
  IDictionaryEnumerator enumr = 
    dict.GetEnumerator();
  if(enumr.MoveNext()) { // returns false at end
    string key = (string)enumr.Key;
    int? value = null;
    if(enumr.Value != null) {
      value = (int)enumr.Value;
    }
    // do something with key and value
  }
  /*-- alternate evaluation --*/
  List<string> keys = dict.Keys.ToList();
  if(keys.Count > 0) {
    string keyfirst = keys[0];
    int valfirst = dict[keyfirst];
    // do something with key and value
  }

            
List<T> is sequential container with an expandable number of elements all of type tεT. New lists are initialized with a braced list of elements of the specified type. Element values are accessed via indexing, and new values can be added, inserted, and removed. Dictionary<K, V> is an associative container of values accessed with keys. The associative containers use a table of buckets, where each bucket is a linked list of KeyValuePairs. An item is accessed with a hashed address followed by a walk up the bucket list looking for the specified key to find its value. The table size is adjusted at run-time to keep the bucket lists short, e.g., one or two items. That means that value access via a key is nearly constant time, e.g., the time to compute the hashed address. KeyValuePairs are added using Add(Key,Value) or by "indexing" with Dict[key] = value. All of the keys and values can be accessed sequentially using an enumerator since Dictionary<K, V> implements the IEnumerable<T> interface. Individual key-value pairs can be accessed from the dictionary's key collection, e.g., dict.keys.ToList(). Collection elements can also be accessed using C#'s LINQ facility. That provides an SQL like programming interface:
Query a collection of objects
Output
  /*-- List<int> li = new List<intgt; { 1, 2, 3, 2, 1 }; --*/
  li: Type: List`1
  value: System.Collections.Generic.List`1[System.Int32]
  size: 8
  /*-- li.Insert(5, 0) --*/
  List<int> { 1, 2, 3, 2, 1, 0 }

  /*-- Dict dict = new Dict(); --*/
  dict: Type: Dictionary`2
  value: System.Collections.Generic.Dictionary`2[ ...
  size: 8
  dict: { [three, 3], [zero, 0], [one, 1], [two, 2] }

            
Initialization and display of List<int> and Dictionary<string, int> are shown here. The output functionalities of Console.Write and String.Format do not enumerate through collection types. Instead they display the compiler's version of the collection type name. Enumerated values shown here were generated from generic functions defined in AnalysisData.cs , found in the Bits Repository . These functions use C# syntax we won't cover until the C# Generics bit. We will discuss them in some detail in that Bit. You don't need to understand how they work for now.
  • ToStringRepIEnumerable<List<int>, int>(li)
  • ToStringRepAssocCont<Dict,string,int>(dict)
shown in Section 3.0, below.

2.0 Copy Operations

This section presents basic type operations, e.g., copy construction and copy assignment. We will see that for value types these operations do bit-wise copy of values from source to destination. For reference types only references are copied, not the values they refer to. The purpose of this section is to explore the consequences of that behavior.
  /*--- copy value type ---*/

  int i = 42;
  string addri = ToStringAddress<int>(&i);

  int j = i;  // copy of value
  string addrj = ToStringAddress<int>(&j);

  /*--- copy reference ---*/

  List<int> li = new List<int> { 1, 2, 3, 2, 1 };
  string addrli = ToStringAddress<List<int>>(&li);

  List<int> lj = li;  // copy of ref
  string addrlj = ToStringAddress<List<int>>(&lj);
            
Since i and j are value types, e.g., integers, the copy construction operation shown is a bit-wise copy of the value of the source i into the newly created destination j. The lists li and lj are reference types. That is, li is not an instance of a list, it is a handle to an instance of a list in the managed heap. The copy construction operation shown creates a new location to store a handle for lj and copies the li's reference into that location. The result is two handles pointing to the same underlying instance of a list in the managed heap. It is unfortunate that the same syntax results in very different semantics for value types and reference types. The ToStringAddress<T>(T* ptr) function creates a string representation of the specified addresses &i, &j, &li, and &lj. Its definition is shown in Section 3.0. Note that pointers can only be used in unsafe C# code blocks. We use the pointers to investigate consequences of copying. They are not dereferenced in any part of this code. If you examine the output provided below, you will see that the address data verifies claims made in this text.
Output
  -----------------------------------
  Demonstrate Copy Operations
  -----------------------------------

  --- int i = 42; ---
  i: address: 0xb2f397e958
  --- int j = i; // copy of value ---
  j: address: 0xb2f397e948
  --------------------------------------------------
  Addresses of i and j are unique, demonstrating
  value of i was copied to new j location.
  --------------------------------------------------

  --- List<int> li = new List<int> { 1, 2, 3, 2, 1 } ---
  li: address: 0xb2f397e938
  --- List<int> lj = li  // copy of reference ---
  lj: address: 0xb2f397e928
  -------------------------------------------------------
  Addresses of li and lj are adjacent, and adjacent to
  addresses of i and j in the stack frame.

  That demonstrates that lj is a copy of the handle li
  both of which point to the managed heap-based list
  instance.
  -------------------------------------------------------

  --- lj.Add(-1) ---
  lj: { 1, 2, 3, 2, 1, -1 }
  li: { 1, 2, 3, 2, 1, -1 }
  -------------------------------------------------------
  Note: changing lj results in the same change to li.
  This demonstrates that both variables refer to the
  same List<int> instance in the managed heap.
  -------------------------------------------------------

  Using ReferenceEquals(li, lj) we find:

    li is same object as lj
            
The first few lines illustrate a copy of the integer value type i into a newly created integer j. This is a byte-wise copy of one location in the stack frame to another. The next lines demonstrate a copy of the reference type handle li to a new location in the stack frame for lj. this is a byte-wise copy of the handle, not the heap-based List<int> it points to. Both handles refer to the same managed instance. Those semantics are demonstrated by adding an element using the new handle lj, i.e. lj.Add(-1), and observing that the original li now points to the modified list. Finally, we confirm those results using C#'s ReferenceEquals method from the Object class.

3.0 Analysis and Display Functions

When you look at any of the "Output" details, you will see some output with detailed formatting, but you won't see code providing that output in corresponding code sections. Code responsible for formatting and supplying low-level details, like type information, has been elided from the examples shown above. The elided code consists of calls to functions shown in the dropdown below. These functions use language features, like generics, that will be covered in later Bits. You can find the complete code, including all the elisions, in the Bits Repository.
Functions 
  class Anal {

    /*--------------------------------------------- 
      display the type of t using reflection 
    ---------------------------------------------*/
    public static void ShowType<T>(
      T t, String nm, String suffix = ""
    )
    {
      #pragma warning disable CS8602 // possible null ref
      Type tt = t.GetType();
      Console.WriteLine("{0}: Type: {1}", nm, tt.Name);
      int size = Anal.GetManagedSize(tt);
      Console.WriteLine(
        "value: {0}\nsize: {1}{2}", t, size, suffix
      );
      #pragma warning restore CS8602
    }
    /*---------------------------------------------
      return a string with type information 
    ---------------------------------------------*/
    public static string GetTypeString<T>(
      T t, String nm, String suffix = ""
    ) {
      #pragma warning disable CS8602 // possible null ref
      Type tt = t.GetType();
      string typeInfo = 
        String.Format("{0}: Type: {1}\n", nm, tt.Name);
      int size = Anal.GetManagedSize(tt);
      string instanceInfo = 
        String.Format(
          "value: {0}\nsize: {1}{2}", t, size, suffix
        );
      return typeInfo + instanceInfo;
      #pragma warning restore CS8602
    }
    /*--------------------------------------------- 
      do t1 and t2 refer to same object? 
    ---------------------------------------------*/
    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
      );
      }
    }
    /*---------------------------------------------
      - GetMangedSize(Type type) is function that
        returns size of value types and handles.
      - It uses advanced techniques that will 
        eventually be covered elsewhere in this
        site. Knowing how it works is not essential 
        for things we are examining in this demo.
      - uses advanced relection
    ---------------------------------------------*/
    // https://stackoverflow.com/questions/8173239/...
    public static int GetManagedSize(Type type)
    {
      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());
    }
    /*--------------------------------------------- 
      return string rep of argument's address 
    ---------------------------------------------*/
    #pragma warning disable 8500
    /*
      Suppress warning about taking address of 
      managed type. Pointer is used only to show 
      address of ptr as part of analysis of copy 
      operations.
    */
    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
  }
  class Display
  {
    /*---------------------------------------------
      show string description of operation 
    ---------------------------------------------*/
    public static void ShowOp(
      String op, String suffix = ""
    ) {
      Console.WriteLine(
        "--- {0} ---{1}", op, suffix
      );
    }
    /*--------------------------------------------- 
      Display all elements of object's tree
      - uses advanced reflection
    ---------------------------------------------*/
    // https://stackoverflow.com/questions/7613782/...
    public static void Iterate<T>(T t) {
      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
        );
      }
    }
    /*---------------------------------------------
      shorthand for console write command 
    ----------------------------------------------*/
    public static void Print(String s = "") {
      Console.WriteLine(s);
    }
    /*---------------------------------------------
      Show text wrapped in horizontal lines
    ---------------------------------------------*/
    public static void ShowNote(
      string s, string suffix = "", int length = 35
    ) {
      string line = new string('-', length);
      Console.WriteLine(line);
      Console.WriteLine("  {0}", s);
      Console.WriteLine(line + suffix);
    }
    /*---------------------------------------------
      Build string rep 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 rep 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();
  }
}
            
These functions are all static member functions of one of two classes. Class Anal holds functions that analyze types and their operations. Class Display holds functions that supply text information to the output. Many of these use reflection, which we have not covered yet. It is not critical that you understand all the details. Just knowing what they do is sufficient for this Bit. We will cover some of them in detail in the Generics Bit.

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\CSharp_Data. Figure 1. VS Code IDE - C# Data Figure 2. C# Data Launch.JSON

5.0 References

Reference Description
C# Type System - Microsoft Discussion with examples
C# Type Reference - Microsoft Semi-formal description of C# Types