about
Bits: C# Data
11/25/2023
0
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 holdnull by default and may be assigned a null value as well as non-null values. The value can be tested withNullable<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
Table 1.0 C# Types
Type | Comments | Example |
---|---|---|
-- Integral value types ---- | ||
values true and false | ||
unsigned 8-bit integer | ||
signed 8-bit integer | ||
with short, ushort, uint, long, ulong qualifiers |
|
|
|
nint and unint are signed and unsigned native size integers. Size is platform dependent. | |
|
16 bit unsigned value | |
-- Floating point value types ---- | ||
size = 4 bytes, values have finite precision, and may have approximate values | ||
size = 8 bytes, values have finite precision, and may have approximate values | ||
size = 16 bytes, values are exact, has no exponent | ||
-- Aggregate types ---- | ||
Reference type, array of N elements, all of type |
int first = arr[0]; |
|
Tuple value type, collection of heterogeneous types accessed by position |
char third = tup.Item3; |
|
Value type, collection of heterogeneous types accessed by name |
S s; s.I = 42; s.D = 3.14159; int first = s.I; |
|
-- Reference types defined by C# language ---- | ||
Base for all value and reference types with methods |
string type = o.GetType().ToString(); |
|
Expandable collection of Unicode characters allocated in the managed heap |
|
|
Reference type that encapsulates a method | ||
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. |
Console.WriteLine("dyn: {0}", dyn); dyn = 3.1415927; Console.WriteLine("dyn: {0}", dyn); |
|
|
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 ---- | ||
Expandable list of elements of type |
new List<int> { 1, 2, 3, 2, 1 }; int lfirst = li[0]; |
|
Unordered associative container of Key-Value pairs, held in table of buckets. |
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 |
Intermediate strength typing |
Types are exhaustively checked but allow both implicit and explicit conversions.
|
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., |
1.0 Initialization
1.1 Value Types
/*---------------------------------
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;
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
1.2 Language-defined Aggregate Types
/*-- 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;
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 }
1.3 Language-defined Reference Types
/*-- 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;
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 }
1.4 Library-defined Reference 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
}
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] }
-
ToStringRepIEnumerable<List<int>, int>(li) -
ToStringRepAssocCont<Dict,string,int>(dict)
2.0 Copy Operations
/*--- 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);
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
3.0 Analysis and Display Functions
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();
}
}
4.0 VS Code View
5.0 References
Reference | Description |
---|---|
C# Type System - Microsoft | Discussion with examples |
C# Type Reference - Microsoft | Semi-formal description of C# Types |