Value Types and Reference Types
LINQ (Language Integrated Query) in C#
Default Interface Methods in C#
File-scoped Namespace Declaration
C# 10.0 - Extended Property Patterns
Natural Type Expressions in C# 10.0
Global Using Directives in C# 10.0
File-scoped Namespace Enhancement in C# 10.0
UTF-8 String Literals in C# 11.0
Enhanced #line Directive in C# 11.0
Xamarin for Mobile Development
Unit Testing in C# with NUnit and xUnit
Concurrency and Multithreading in C#
Performance Optimization in C#
Cross-Platform Development with .NET Core
Application Lifecycle Management with C#
Source Control Integration in C#
Continuous Integration/Continuous Deployment (CI/CD) with C#
Cloud Services Integration in C#
Windows Presentation Foundation (WPF)
Universal Windows Platform (UWP) Basics
Exploring C# Interactive (CSI)
Code Refactoring Tools in Visual Studio
Localization and Globalization in C#
Data Types and Variables in C#
Object-Oriented Programming in C#
Game Development with Unity and C#
◆
Welcome to a tailored learning journey in the world of C# programming. Designed for indivi duals who already grasp basic programming concepts, this book aims to equip beginners wit h the essential knowledge necessary to master C#. Each section of this guide is crafted to ens ure that you gain a deep understanding of key C# elements without overwhelming details.
Whether you're starting out or revisiting the fundamentals as a seasoned programmer, this b ook serves as a focused resource to brush up on the essentials. Our concise approach allows you to efficiently learn and apply your skills in practical scenarios.
We encourage you to leave a review or comment after your reading experience. Your feedba ck not only helps us improve but also assists your fellow engineers in discovering this valuabl e resource. Sharing your thoughts and insights can greatly benefit others in similar positions, fostering a community of learning and growth.
Static Typing
Static typing in C# means that variable types are explicitly declared and determined at compi le time.
In the following example, we assign integers and strings to variables, demonstrating C#’s stat ic typing.
[Code]
int number = 5;
string greeting = "Hello, world!";
Console.WriteLine(number);
Console.WriteLine(greeting);
[Result]
5
Hello, world!
In this example, the variable number is explicitly declared as an int, and greeting is declared a s a string. This is essential in C# because the type of each variable is fixed at compile time an
d cannot change throughout the program, which helps prevent many common type-related errors that can occur in dynamically typed languages. The explicit type declaration enhances code readability, debugging, and performance optimization, as the compiler can make more assumptions and optimizations.
[Trivia]
Static typing helps catch errors at compile time rather than at runtime, which generally result s in more robust and maintainable code. It also allows Integrated Development Environment s (IDEs) to provide features like type inference, code completion, and more effective refactori ng tools.
Value Types and Reference Types
C# distinguishes between value types and reference types, which are stored and handled diff erently in memory.
Below is an example showcasing the difference between a value type and a reference type.
[Code]
int value1 = 10;
int value2 = value1;
value2 = 20;
Console.WriteLine("Value1: " + value1); // Output will be based on the value type behavior Console.WriteLine("Value2: " + value2); // Demonstrates independent copy behavior string ref1 = "Hello";
string ref2 = ref1;
ref2 = "World";
Console.WriteLine("Ref1: " + ref1); // Output will reflect reference type behavior Console.WriteLine("Ref2: " + ref2); // Shows independent reference due to string immutabilit y
[Result]
Value1: 10
Ref1: Hello
Ref2: World
In the value type example (int), changing value2 does not affect value1 because when value2
is assigned value1, a new independent copy of the value is created. In contrast, with referenc e types (string in this case), both ref1 and ref2 initially point to the same data. However, strin gs are immutable in C#, so when ref2 is changed, it actually points to a new string object, lea ving ref1 unchanged. This behavior is crucial for understanding how memory management a nd data manipulation work in C#, affecting performance and functionality.
[Trivia]
Understanding the distinction between value types and reference types is essential for mana ging memory efficiently in C#. Value types are stored on the stack, which allows quicker acce ss but limited flexibility. Reference types are stored on the heap, which is more flexible but re quires overhead for memory management.4
Properties in C#
Properties in C# are members that provide a flexible mechanism to read, write, or compute t he values of private fields.
The following example illustrates a simple class with a private field and a property that provid es access to this field.
[Code]
public class Person
{
private string name; // Private field
// Public property
public string Name
{
get { return name; } // Get method
set { name = value; } // Set method
}
}
class Program
{
static void Main()
{
Person person = new Person();
person.Name = "Alice"; // Using the set accessor Console.WriteLine(person.Name); // Using the get accessor
}
}
[Result]
Alice
In C#, properties play a crucial role in encapsulation, one of the fundamental principles of obj ect-oriented programming. They allow the class to control how important fields are accessed and modified. In our example:private string name;: This line declares a private field named na me. This field cannot be accessed directly from outside the class, which protects the field fro m unwanted external modifications.public string Name: This property acts as a safe way to ac cess the private field. It includes two parts:get { return name; }: This is a get accessor, used to return the value of the private field. When someone outside the class wants to get the value of Name, this accessor is invoked.set { name = value; }: This is a set accessor, used to assign a new value to the private field. The value keyword represents the value being assigned to the property.Properties can also be read-only (if they have no set accessor) or write-only (if they have no get accessor), depending on the needs of your program.
[Trivia]
In more advanced scenarios, properties can use more complex logic in get and set accessors, not just simple assignments. For example, you could add validation in the set accessor to che ck the incoming value before setting the field.
Indexers in C#
Indexers allow instances of a class or struct to be indexed just like arrays.
The following example demonstrates a class that simulates an internal array through an inde xer.
[Code]
public class SimpleArray
{
private int[] array = new int[10]; // Internal private array
// Indexer definition
public int this[int index]
{
get { return array[index]; } // Get indexer
set { array[index] = value; } // Set indexer
}
}
class Program
{
static void Main()
{
SimpleArray sa = new SimpleArray();
sa[0] = 10; // Using the set indexer
sa[1] = 20; // Using the set indexer Console.WriteLine(sa[0]); // Using the get indexer
Console.WriteLine(sa[1]); // Using the get indexer
}
}
[Result]
10
20
Indexers in C# are a syntactic convenience that allows an object to be indexed like an array.
Here’s a deeper look at the indexer used in the example:private int[] array = new int[10];: This private field holds an array of integers. The array is used internally to store data.public int this
[int index]: This is the syntax for declaring an indexer. The this keyword is followed by a para meter list enclosed in square brackets. The parameter indicates the index position.get { retur n array[index]; }: The get accessor for the indexer returns the value at the specified index.set {
array[index] = value; }: The set accessor assigns a value to the array at the specified index. Th e value keyword represents the value being assigned.Indexers can be very powerful, especiall y in classes that encapsulate complex data structures. They provide a way to access elements in a class that encapsulates a collection or array with the simplicity of using array syntax.
[Trivia]
You can define indexers not only for single parameters but also for multiple parameters, enh ancing the capability to mimic multi-dimensional arrays or more complex data structures.4
Events in C#
Events in C# are a way to send notifications to multiple subscribers when something significa nt happens in your program.
Below is a simple example demonstrating how to declare and use events in C#. We'll create a class that triggers an event when a method is called.
[Code]
using System;
public class ProcessBusinessLogic
{
// Declare the delegate
public delegate void ProcessCompletedEventHandler(object sender, EventArgs e);
// Declare the event
public event ProcessCompletedEventHandler ProcessCompleted;
// Method to trigger the event
public void StartProcess()
{
Console.WriteLine("Process Started.");
// Some process logic here
OnProcessCompleted(EventArgs.Empty);
}
protected virtual void OnProcessCompleted(EventArgs e)
// Check if there are any subscribers
ProcessCompleted?.Invoke(this, e);
}
}
class Program
{
static void Main(string[] args)
{
ProcessBusinessLogic pbl = new ProcessBusinessLogic(); pbl.ProcessCompleted += (sender, e) => Console.WriteLine("Process Completed."); pbl.StartProcess();
}
}
[Result]
Process Started.
Process Completed.
In the provided example, we have a class called ProcessBusinessLogic. This class contains a d elegate named ProcessCompletedEventHandler, which defines the signature for methods tha t can respond to the event. The event itself, ProcessCompleted, uses this delegate type.The method StartProcess is used to simulate a process. When this method is called, it eventually t riggers the ProcessCompleted event by calling OnProcessCompleted, which checks if there ar e any subscribers to the event. If there are, it invokes the event, passing itself (this) and an Ev entArgs object to the subscribers.This pattern allows other parts of your application to react t o changes or significant actions within the class without tightly coupling the components.
[Trivia]
Events in C# are built on the delegate model. An event can have multiple subscribers, and wh en the event is triggered, all subscribers are notified in the order they subscribed. This is parti cularly useful in designing loosely coupled systems and is a key aspect of the observer desig n pattern.
Delegates in C#
Delegates are type-safe function pointers in C# that allow methods to be passed as paramet ers.
Below is a basic example showing how to declare, instantiate, and use delegates in C#. We'll create a delegate that points to a method performing a simple calculation.
[Code]
using System;
public delegate int CalculationHandler(int x, int y);
class Program
{
static int Add(int a, int b)
{
return a + b;
}
static void Main(string[] args)
{
CalculationHandler handler = Add;
int result = handler(5, 6);
Console.WriteLine("Result of addition: " + result);
}
}
[Result]
Result of addition: 11
In the example, a delegate named CalculationHandler is defined that can reference any meth od that takes two integers as parameters and returns an integer. In this case, the delegate ref erences the Add method.The Main method creates an instance of CalculationHandler, pointi ng it to the Add method. When handler is invoked with two integers, it effectively calls Add with those integers and outputs the result.This demonstrates how delegates are used to enca psulate a method reference and pass it around like any other variable or parameter. This is in strumental in creating flexible and reusable components such as event handling systems or c allback mechanisms.
[Trivia]
Delegates are the foundation of many advanced .NET framework features such as events and LINQ. They are integral to understanding asynchronous programming patterns in C#, includi ng the use of async and await keywords.4
Lambda Expressions in C#
Lambda expressions in C# provide a concise way to write inline expressions or functions that can be used to create delegates or expression tree types.
Here is a simple example of a lambda expression used to filter a list of integers.
[Code]
List<int> numbers = new List<int> { 1, 2, 3, 4, 5 }; Func<int, bool> isEven = x => x % 2 == 0;
List<int> evenNumbers = numbers.Where(isEven).ToList(); foreach (var num in evenNumbers)
{
Console.WriteLine(num);
}
[Result]
2
4
In this code, List<int> numbers initializes a list of integers. The lambda expression x => x % 2
== 0 defines a function that checks if a number is even. This function is used as a delegate F
unc<int, bool>, where int is the input type and bool is the return type, indicating the result o f the condition.The method Where(isEven) is a LINQ method that filters the list based on the l ambda function. Only numbers satisfying the condition (even numbers) are included in the n ew list evenNumbers, which is materialized into a list with .ToList(). The foreach loop then iter ates over evenNumbers and prints each element.Lambda expressions are essential for writing concise and readable code, particularly when working with data manipulation and LINQ.
[Trivia]
Lambda expressions in C# are derived from lambda calculus, which is a framework develope d in the 1930s to study functions and their application. In programming, they provide a powe rful tool for creating concise and flexible functional-style code.
LINQ (Language Integrated Query) in C#
LINQ is a powerful feature in C# that allows developers to query various data sources (like arr ays, enumerable classes, XML, relational databases) in a consistent manner.
Below is an example of using LINQ to query an array of names to find names that contain the letter 'a'.
[Code]
string[] names = { "Alice", "Bob", "Carol", "David" }; var namesWithA = from name in names
where name.Contains('a')
select name;
foreach (var name in namesWithA)
{
Console.WriteLine(name);
}
[Result]
Alice
Carol
David
This example initializes an array string[] names with several names. The LINQ query is written using query syntax. It starts with the from clause, which specifies the data source (names). Th e where clause filters names containing the letter 'a'. The select statement specifies that thes e names should be selected into the resulting sequence namesWithA.The query syntax in LIN
Q closely resembles SQL (Structured Query Language), making it intuitive for those familiar w ith database querying. The result is executed lazily; the actual query execution occurs at the f oreach loop, where each filtered name is printed.LINQ simplifies data querying by integrating query capabilities directly into the C# language, allowing for more readable and maintainabl e code.
[Trivia]
LINQ queries can be written in two ways: query syntax (shown above) or method syntax, whic h uses extension methods and lambda expressions. Both compile to the same code, but met hod syntax can be more flexible and powerful for complex queries.4
Nullable Types in C#
Nullable types in C# allow value types, like integers and booleans, which inherently cannot b e null, to represent the absence of a value.
The following example demonstrates how to declare and use nullable types, specifically a nul lable integer.
[Code]
int? nullableInt = null;
Console.WriteLine("Nullable integer value: " + nullableInt);
[Result]
Nullable integer value:
In C#, value types like int, double, and bool cannot be null because they directly contain data
. However, in many programming scenarios, such as database operations or dealing with ext ernal services, you might encounter situations where a value type needs to represent the abs ence of a value (null). C# provides nullable types using the syntax int?, double?, bool?, etc., w here the question mark indicates that the type can hold either a value or null.The nullable typ e is actually an instance of the System.Nullable<T> struct, which provides handy properties li
ke HasValue (returns true if the nullable type has a value) and Value (returns the value if Has Value is true, otherwise throws an InvalidOperationException). This makes nullable types a po werful feature for handling data that might not always be present.
[Trivia]
When working with nullable types, you can use the GetValueOrDefault() method which retur ns the default value for the underlying type if the nullable type has no value. This is particular ly useful in conditional statements or calculations where using null directly could cause errors
.
Async/Await in C#
The async/await pattern in C# is used to handle asynchronous operations, enabling your appl ication to remain responsive while performing long-running tasks like file I/O or network req uests.
The following example shows a simple asynchronous method using async and await that sim ulates a task that takes time, such as fetching data from a server.
[Code]
async Task<string> FetchDataAsync()
{
// Simulate a task that takes 3 seconds to complete
await Task.Delay(3000);
return "Data retrieved";
}
async Task MainAsync()
{
string result = await FetchDataAsync();
Console.WriteLine(result);
}
MainAsync().GetAwaiter().GetResult();
Data retrieved
In this example, FetchDataAsync is an asynchronous method defined with the async keyword, which tells the compiler that the method contains an await expression. await is used before c alling any method that returns a Task or Task<T>, which are types used for representing ong oing work.await essentially tells the compiler, "Pause executing this method until the awaited task completes, but don't block the thread while waiting." This allows other operations to run in the meantime, like UI updates or other concurrent tasks, which keeps your application resp onsive.In the MainAsync method, await is used again to asynchronously wait for FetchDataAs ync to complete before continuing with the rest of the method. This pattern avoids the comp lexities and potential deadlocks associated with other asynchronous programming technique s, such as callbacks or manual thread handling.
[Trivia]
The C# compiler transforms methods containing async and await into state machines, which manage the complexities of asynchronous operations. This transformation allows the asynchr onous method to pause and resume at await points without blocking the thread on which it r uns, enhancing the scalability and performance of applications.4
Exception Handling in C#
Exception handling in C# is a critical concept for building robust applications that can gracef ully handle runtime errors.
The following example demonstrates how to use try-catch blocks to handle exceptions.
[Code]
using System;
class Program
{
static void Main()
{
try
{
Console.WriteLine("Enter a number to divide 100:"); int divisor = Convert.ToInt32(Console.ReadLine());
int result = 100 / divisor;
Console.WriteLine("100 divided by {0} is {1}", divisor, result);
}
catch (DivideByZeroException)
{
Console.WriteLine("Cannot divide by zero, please provide a valid number.");
}
{
Console.WriteLine("Not a valid number, please enter an integer.");
}
finally
{
Console.WriteLine("Operation attempted");
}
}
}
[Result]
If a user enters '0', the output will be:csharpCopy codeCannot divide by zero, please provide a valid number.
Operation attempted
If a non-numeric string is entered, the output will be:bashCopy codeNot a valid number, plea se enter an integer.
Operation attempted
In C#, try blocks are used to wrap code that might throw an exception, while catch blocks are used to handle the exceptions if they are thrown. Each catch block can handle a specific type of exception. The finally block is optional and executes whether or not an exception is throw n, making it ideal for cleaning up resources, such as closing files or releasing network connec tions.
[Trivia]
The DivideByZeroException and FormatException are both common exceptions in C#. It is go od practice to catch more specific exceptions before more general ones to provide clearer er ror messages and actions.
Attributes in C#
Attributes in C# provide a powerful way to add metadata or declarative information to code elements like classes, methods, or properties.
Here, we use the [Obsolete] attribute to mark a method as deprecated.
[Code]
using System;
public class Calculator
{
[Obsolete("Use AddNumbers method instead.")]
public int Add(int a, int b)
{
return a + b;
}
public int AddNumbers(int x, int y)
{
return x + y;
}
}
class Program
{
static void Main()
Calculator calc = new Calculator();
int sum = calc.Add(5, 10); // Intended to trigger a compiler warning Console.WriteLine("Sum: " + sum);
}
}
[Result]
The code will compile, but it will generate a compiler warning:csharpCopy code'Calculator.Ad d(int, int)' is obsolete: 'Use AddNumbers method instead.'
Sum: 15
Attributes in C# are placed above a class, method, or property using square brackets. They ca n instruct the compiler to do something (like issue a warning), or they can be used to attach metadata that can be read at runtime through reflection. This example uses the Obsolete attr ibute to indicate that a method should no longer be used. It serves as a helpful tool for devel opers during code upgrades or maintenance to avoid using outdated methods.
[Trivia]
Attributes can be custom-defined, allowing developers to create their own metadata annotat ions for special processing. This can be particularly useful in large projects for enforcing stan dards or enhancing functionality without modifying existing code directly.4
Reflection in C#
Reflection is a powerful feature in C# that allows programs to inspect and modify their own s tructure and behavior at runtime.
The following example demonstrates how to use reflection to inspect a class's properties and create an instance of the class dynamically.
[Code]
using System;
using System.Reflection;
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
public void DisplayInfo()
{
Console.WriteLine($"Name: {Name}, Age: {Age}");
}
}
class Program
{
static void Main()
{
// Using reflection to get type information from the Person class Type personType = typeof(Person);
// Creating an instance of Person dynamically
object personInstance = Activator.CreateInstance(personType);
// Setting properties dynamically
PropertyInfo nameProp = personType.GetProperty("Name"); PropertyInfo ageProp = personType.GetProperty("Age"); nameProp.SetValue(personInstance, "John Doe"); ageProp.SetValue(personInstance, 30);
// Invoking a method dynamically
MethodInfo displayMethod = personType.GetMethod("DisplayInfo"); displayMethod.Invoke(personInstance, null);
}
}
[Result]
Name: John Doe, Age: 30
In the example above, the Person class defines two properties: Name and Age, and a method DisplayInfo that prints these properties. Using reflection, the Main method in the Program cl ass retrieves the Type of the Person class, creates an instance of it, sets its properties, and inv okes its method, all dynamically. This capability is particularly useful in scenarios where the ty pe of objects to create or manipulate is not known at compile time.Reflection can be resourc e-intensive and can potentially expose private data, so it should be used judiciously and with an awareness of security and performance implications.
[Trivia]
Reflection provides the ability to obtain metadata about assemblies, modules, and types. You can use it to discover information at runtime that is typically available at compile time, which is especially useful for applications that require a high degree of flexibility or need to interact with unknown, dynamically loaded types.
Generics in C#
Generics increase the flexibility and reusability of code by allowing you to define classes, met hods, and interfaces with placeholders for the types they operate on.
The following example shows how to create a generic class and method, demonstrating the power of type safety and code reuse.
[Code]
using System;
public class GenericList<T>
{
private T[] items;
private int count;
public GenericList(int capacity)
{
items = new T[capacity];
}
public void Add(T item)
{
if (count >= items.Length)
throw new InvalidOperationException("List is full"); items[count++] = item;
}
{
if (index < 0 || index >= count)
throw new ArgumentOutOfRangeException(nameof(index), "Index out of range"); return items[index];
}
}
class Program
{
static void Main()
{
// Creating an instance of a generic class
GenericList<string> list = new GenericList<string>(10); list.Add("Hello");
list.Add("World");
// Displaying items
Console.WriteLine(list.GetItem(0));
Console.WriteLine(list.GetItem(1));
}
}
[Result]
Copy codeHello
World
In the code above, GenericList<T> is a custom class that implements a generic list capable of storing any type, as specified by the placeholder <T>. This allows the class to be used with a ny type, making it extremely versatile and type-safe. Type safety means that the compiler will
enforce that only the correct type of data is added to the list, reducing runtime errors.Generi cs also improve performance by eliminating the need to cast objects and reducing the need f or boxing and unboxing when dealing with value types.
[Trivia]
Generics were introduced in C# 2.0. They are implemented at the CLR level, meaning that the y offer performance benefits by avoiding runtime type checks and the overhead associated w ith non-generic collections, like ArrayList, where types must be cast to and from object, pote ntially leading to type mismatch errors.4
Extension Methods
Extension methods enable you to add new methods to existing types without modifying the original source code or using inheritance.
The following example shows how to define and use an extension method to add functionali ty to the string type.
[Code]
using System;
namespace ExtensionMethodsDemo
{
public static class StringExtensions
{
// Extension method to count the number of words in a string public static int WordCount(this string str)
{
return str.Split(new char[] { ' ', '.', '?' }, StringSplitOptions.RemoveEmptyEntries).Length;
}
}
class Program
{
static void Main(string[] args)
{
string exampleSentence = "Hello, world! How are you today?"; int count = exampleSentence.WordCount();
Console.WriteLine($"The sentence has {count} words.");
}
}
}
[Result]
The sentence has 6 words.
In the code above, StringExtensions is a static class that contains a static method WordCount.
The this keyword before the string str parameter indicates that WordCount is an extension m ethod for the string type. This method counts the number of words in the string by splitting i t based on spaces, periods, and question marks, excluding empty entries. The WordCount m ethod can be called on any string instance as if it were a method originally defined in the stri ng class.This functionality is particularly useful for adding methods to types for which you do not have the source code, like built-in .NET classes or third-party classes. Extension methods must be defined in a non-generic, static class and the method itself must also be static.
[Trivia]
The first parameter of an extension method must have the this keyword followed by the type the method will extend. This tells the compiler that it is an extension method. Additionally, ex tension methods can be used to add LINQ capabilities to custom collections.
Partial Classes and Methods
Partial classes and methods allow a class or method to be split across multiple files, facilitatin g modular programming and collaborative development.
Here is a simple example demonstrating how a partial class can be used to split the impleme ntation of a class across two files.
[Code]
// File: MyClass.cs
using System;
namespace PartialClassesDemo
{
public partial class MyClass
{
public void MethodA()
{
Console.WriteLine("Method A");
}
}
// File: MyClassExtensions.cs
public partial class MyClass
{
public void MethodB()
Console.WriteLine("Method B");
}
}
class Program
{
static void Main(string[] args)
{
MyClass myClass = new MyClass();
myClass.MethodA();
myClass.MethodB();
}
}
}
[Result]
Method A
Method B
The example shows two parts of the MyClass class, each defined in separate files (MyClass.cs and MyClassExtensions.cs). Both parts are marked with the partial keyword, which indicates t hat they are parts of the same class. When the program is compiled, these parts are combine d into a single class definition.Partial classes are useful when working with automatically gen erated code, such as in the case of Windows Forms, or when working in large teams where di fferent team members work on different parts of a class. They also help keep individual files c lean and focused on specific aspects of class functionality.Partial methods, not shown in this example, work similarly but must be declared within partial classes. They allow method signat ures to be declared in one part of a partial class and optionally implemented in another. If no
t implemented, the calls to them are ignored at compile time, resulting in no performance pe nalty.
[Trivia]
When using partial classes, all parts must be compiled together; otherwise, the compiler will t reat them as incomplete. Additionally, all parts must have the same access level (e.g., public, private), and they are often used in scenarios involving large teams or complex project struct ures, where maintaining a single file for a class might be impractical.4
Anonymous Types in C#
Anonymous types provide a convenient way to encapsulate a set of read-only properties into a single object without having to explicitly define a type first.
The following C# code example demonstrates how to create an anonymous type with variou s properties.
[Code]
var anonymousType = new { Name = "John Doe", Age = 30, Country = "USA" }; Console.WriteLine($"Name: {anonymousType.Name}, Age: {anonymousType.Age}, Country: {a nonymousType.Country}");
[Result]
Name: John Doe, Age: 30, Country: USA
In C#, anonymous types are typically used for data that does not require a formal class defini tion, and they are defined using the new keyword followed by the object initializer syntax. Th is feature is particularly useful in LINQ queries where you want to return a subset of propertie s from an object. The properties of anonymous types are read-only, meaning once they are i nitialized, their values cannot be changed.Each property in the anonymous type must be initi
alized, and the compiler automatically determines the most appropriate type. In the example above, Name is inferred to be of type string, Age of type int, and Country also as string. Ano nymous types are usually used in local scope and cannot be returned from methods unless r eturned as type object or used within a generic like IEnumerable<T>.
[Trivia]
Anonymous types are internally managed by the compiler using a feature called type inferen ce. This feature helps the compiler automatically detect and assign the most appropriate dat a type based on the provided value. Moreover, the actual class name for an anonymous type is generated by the compiler and is not accessible at design time.
Dynamic Types in C#
The dynamic type in C# enables the operations that are normally checked at compile-time to be resolved at run-time.
This example illustrates how to declare and use a dynamic type, highlighting its flexibility in o perations that are determined during execution.
[Code]
dynamic dynamicVariable = "Hello, world!";
Console.WriteLine(dynamicVariable);
dynamicVariable = 10;
Console.WriteLine(dynamicVariable);
dynamicVariable = new { Name = "Jane", Age = 25 }; Console.WriteLine($"Name: {dynamicVariable.Name}, Age: {dynamicVariable.Age}");
[Result]
Hello, world!
10
Name: Jane, Age: 25
The dynamic type bypasses static type checking. When a variable is declared as dynamic, the compiler does not check the type and members of the variable at compile-time; all checks ar e deferred to run-time. This means you can assign any type of value to a dynamic variable, a nd the operations performed on it (like method calls, property access) are resolved only at ex ecution.Using dynamic types can make your code more flexible but also prone to errors if no t properly handled, as errors are only caught during runtime, leading to potential runtime ex ceptions. It is particularly useful when interacting with COM APIs, dealing with dynamically sh aped data like JSON, or when interoperability with other languages in the .NET environment i s required.
[Trivia]
The use of dynamic types can impact performance due to the overhead of run-time type che cking. It is generally advised to use dynamic types sparingly, only in scenarios where the ben efits of using them outweigh the potential performance penalty and the risk of runtime error s.4
Checked and Unchecked
Understanding overflow handling in C#.
This example demonstrates how to use 'checked' and 'unchecked' contexts in C# to control o verflow behavior in numeric operations.
[Code]
using System;
class OverflowExample {
static void Main() {
int maxInt = int.MaxValue;
try {
// Checked context forces overflow detection
int overflow = checked(maxInt + 1);
Console.WriteLine("This line will not be executed due to an exception.");
} catch (OverflowException) {
Console.WriteLine("Overflow detected!");
}
// Unchecked context ignores overflow
int ignoreOverflow = unchecked(maxInt + 1);
Console.WriteLine("Result with overflow ignored: " + ignoreOverflow);
}
}
[Result]
Overflow detected!
Result with overflow ignored: -2147483648
In C#, arithmetic operations on integers can overflow if the result exceeds the type's range. B
y default, the overflow in arithmetic operations in C# is unchecked, meaning the compiler an d runtime do not throw exceptions if an overflow occurs, instead wrapping around using the two's complement representation.The 'checked' keyword forces the compiler to add overflow checks, and if an overflow occurs, an OverflowException is thrown. This is crucial for applicati ons where data correctness cannot be compromised by silent overflows.On the other hand, '
unchecked' allows operations to overflow without throwing exceptions, which can be useful f or performance-critical code where overflow is either unlikely or irrelevant.Understanding wh en to use each context can help in optimizing performance while ensuring program correctn ess.
[Trivia]
You can set the overflow checking behavior project-wide in Visual Studio under the project p roperties' "Build" tab, modifying the "Check for arithmetic overflow/underflow" setting. This c an be handy to enforce consistent overflow checking across your entire application.
Iterators
Leveraging iterators for custom collection traversal.
This example demonstrates creating and using an iterator in C# to traverse a custom collecti on.
[Code]
using System;
using System.Collections;
class MyCollection : IEnumerable {
private int[] numbers = { 1, 2, 3, 4, 5 };
// Custom iterator
public IEnumerator GetEnumerator() {
for (int i = 0; i < numbers.Length; i++) {
// Yield returns each element one at a time
yield return numbers[i];
}
}
}
class IteratorExample {
static void Main() {
MyCollection collection = new MyCollection();
foreach (int number in collection) {
}
}
}
[Result]
1
2
3
4
5
Iterators in C# are a powerful feature that simplify the task of custom collection traversal with out exposing the underlying implementation. The 'yield return' statement is used within an it erator block to provide a value to the caller and maintain the state of the iterator, allowing fo r sequential access over a set of values.Creating an iterator involves implementing the IEnum erable and IEnumerator interfaces, but using 'yield return' simplifies this by automatically gen erating the necessary code to implement these interfaces.Iterators are particularly useful for l azily generating and consuming sequences, which can improve performance and memory us age in applications where large collections or potentially infinite sequences are involved.
[Trivia]
Iterators are not limited to simple for loops. They can be designed to perform complex traver sal, skip elements, or even apply filters before yielding elements. This makes them incredibly versatile for manipulating data within collections.4
nameof Operator
The nameof operator in C# is used to obtain the simple (unqualified) string name of a variabl e, type, or member.
The following example demonstrates how to use the nameof operator to retrieve the name o f a variable and a class property.
[Code]
public class Car
{
public string Model { get; set; }
public static void Main()
{
var car = new Car();
string variableName = nameof(car);
string propertyName = nameof(Car.Model);
Console.WriteLine("Variable name: " + variableName); Console.WriteLine("Property name: " + propertyName);
}
}
[Result]
Variable name: car
Property name: Model
The nameof operator is particularly useful for reducing errors caused by manually typing the names of variables, properties, or methods in your code. This can be crucial for maintaining c ode that involves data binding, INotifyPropertyChanged, or error handling, where exact nam es are necessary. Using nameof helps keep your code refactor-safe, meaning if the name of t he variable, property, or method changes, the output string will automatically update, reduci ng the risk of bugs.
[Trivia]
nameof was introduced in C# 6.0 as part of Microsoft's efforts to improve code readability an d maintainability. Before nameof, developers had to use string literals for the same purposes, which were error-prone and hard to maintain during refactorings.
Null Conditional Operator
The Null Conditional Operator, often termed as the "null-safe" or "Elvis" operator, simplifies t he process of checking for null before accessing members or elements.
The example below shows how to use the null conditional operator to safely access an object property without risking a NullReferenceException.
[Code]
public class Person
{
public string Name { get; set; }
public static void Main()
{
Person person = null;
string personName = person?.Name;
Console.WriteLine("Person's name: " + (personName ?? "Unknown"));
}
}
[Result]
Person's name: Unknown
The null conditional operator (?.) returns null if the left-hand operand is null instead of throwi ng an exception, allowing the right-hand operand to be safely ignored. When combined with the null coalescing operator (??), it provides a powerful tool for dealing with potentially null objects. This prevents the common error of accessing properties or methods on null objects, which otherwise results in a NullReferenceException. This operator is particularly useful in de eply nested structures or in data-binding scenarios where null references can frequently occu r.
[Trivia]
The null conditional operator was introduced in C# 6.0, together with a slew of features aime d at making null handling in C# easier and reducing boilerplate code around null checks. It e mbodies the principle of reducing the amount of code while increasing safety and readability
.4
String Interpolation in C#
String interpolation is a method in C# that allows you to embed variable values directly withi n string literals, making it easier to create dynamic strings.
Here's a simple example demonstrating how to use string interpolation in C# to combine vari ables with text in a single string.
[Code]
string name = "Alice";
int age = 30;
string message = $"Hello, my name is {name} and I am {age} years old."; Console.WriteLine(message);
[Result]
Hello, my name is Alice and I am 30 years old.
In this example, the $ character before the string literal indicates that this is an interpolated s tring. Inside the curly braces {} you can place any C# expression, such as a variable (name, ag e). The expressions inside the braces are evaluated, and their results are converted to strings and substituted in place of the braces. This feature, introduced in C# 6, greatly simplifies the
process of constructing strings as compared to older methods like concatenation using the +
operator or string.Format method.
[Trivia]
Before the introduction of string interpolation in C# 6, developers had to rely heavily on strin g.Format, which was less readable and more error-prone. Interpolation makes code cleaner a nd easier to understand at a glance.
Pattern Matching in C#
Pattern matching in C# provides a way to check the type or shape of an object and bind vari ables to its values in a concise and readable manner.
Below is an example of using pattern matching in C# to distinguish types and extract values f rom different object types within a single control structure.
[Code]
object obj = 123;
string result = obj switch
{
int n when n > 100 => "The number is large.", int n => "It's an integer.",
string s => "It's a string.",
_ => "Unknown type"
};
Console.WriteLine(result);
[Result]
The number is large.
This code uses the switch statement with pattern matching. Here's a breakdown:The switch k eyword is followed by the object obj.Each case in the switch expression uses a type pattern (i nt, string) to test if obj matches that type.The when keyword allows for additional conditions; in this case, it checks if the integer n is greater than 100.If obj matches a case, the correspon ding string is returned; if no cases match, the default case (_) catches all other types.This exa mple demonstrates the power of pattern matching to perform complex checks and handle m ultiple conditions in a clean and efficient way.
[Trivia]
Pattern matching was significantly enhanced in C# 7 and later versions, adding more capabili ties like recursive patterns and property patterns that allow for more detailed conditions and checks directly in the switch statement.4
Local Functions in C#
Local functions are methods defined within the scope of another method, which can help en capsulate functionality that is not needed outside of that method.
The following example demonstrates a simple local function within a method in C#. The local function AddNumbers is used to perform an addition, which simplifies the main method's lo gic.
[Code]
using System;
class Program
{
static void Main(string[] args)
{
int sum = AddNumbers(5, 10);
Console.WriteLine($"The sum is: {sum}");
int AddNumbers(int a, int b)
{
return a + b;
}
}
}
[Result]
The sum is: 15
In the provided code, the Main method includes a local function named AddNumbers. This f unction takes two integers as parameters and returns their sum. The use of local functions all ows you to define methods that are relevant only within the scope of another method, keepi ng your code organized and preventing the global namespace from being cluttered with hel per functions that are not needed elsewhere.Local functions can capture variables from the e nclosing scope, offering functionality similar to lambda expressions but with the clarity and f ull capability of regular methods. This makes code easier to read and debug, especially when the functionality is complex but only relevant within a single method.
[Trivia]
Local functions are especially useful in implementing algorithms that require repetitive but lo calized operations. By defining these operations as local functions, you can make the main m ethod cleaner and more readable. Additionally, local functions can be both synchronous and asynchronous, providing flexibility in handling various kinds of operations within a method.
Tuples in C#
Tuples provide a way to store a fixed-size set of heterogeneous elements, simplifying the pro cess of returning multiple values from methods without creating a custom class or struct.
This example illustrates how to use tuples to return multiple values from a method in C#. The method GetValues returns a tuple containing an integer and a string.
[Code]
using System;
class Program
{
static void Main(string[] args)
{
(int id, string name) = GetValues();
Console.WriteLine($"ID: {id}, Name: {name}");
}
static (int, string) GetValues()
{
return (1, "Alice");
}
}
ID: 1, Name: Alice
The method GetValues in the example returns a tuple consisting of an integer and a string. In C#, tuples are created using the syntax (Type1 item1, Type2 item2, ...). In the Main method, t uple deconstruction is used to assign the returned values to individual variables id and name, respectively. This feature enhances code readability and eliminates the need for out paramet ers or custom types just to return multiple values from a method.Tuples in C# are not only sy ntactically simple but also type-safe, allowing you to directly access each element with its cor responding type without the need for casting. This type safety is beneficial for maintaining d ata integrity and reducing runtime errors.
[Trivia]
Tuples were introduced in C# 7.0 as part of the language's efforts to simplify coding patterns and increase code clarity. They are extensively used for temporary data grouping where creat ing a formal class or struct would be an overkill. Their value-based equality comparison make s tuples particularly useful in various programming scenarios, such as in hash-based collectio ns or when comparing sets of values directly.4
Discards in C#
Discards are a feature in C# used to intentionally ignore a value returned by an expression or a method when you do not need to use that value anywhere in your code.
In the following example, we use a discard to ignore the tuple's second value which we are n ot interested in.
[Code]
(int, int) ReturnMultipleValues()
{
return (1, 2);
}
void UseDiscard()
{
var (first, _) = ReturnMultipleValues();
Console.WriteLine(first);
}
[Result]
1
Discards are represented by the underscore (_) character and are used when you want to ign ore one or more values in a deconstruction or when calling a method that returns a value tha t you do not need. This helps in improving code clarity and preventing unnecessary variable allocation. It's particularly useful in tuple deconstruction or when using out parameters wher e not all the information returned is useful for your current operation.Discards are not actual variables and thus they do not allocate any storage and they cannot be read. As a result, they help you make your intentions clear, not only to the compiler but also to anyone else readin g your code, indicating that certain data is intentionally being ignored.
[Trivia]
In C# 7.0 and later, discards can be used with out variables, tuple deconstruction, and in patt ern matching (e.g., switch statements) to ignore parts of the data that are not relevant to the logic being implemented. This feature simplifies handling complex data structures without th e need to define unnecessary temporary variables.
Ref Locals and Returns
Ref locals and returns are features in C# that allow methods to return a reference to a variabl e rather than a copy of the variable. This can enhance performance, especially for large data structures.
Below is an example showing how to use ref locals and returns to modify the original data st ored in an array.
[Code]
int[] numbers = { 1, 2, 3, 4, 5 };
ref int FindNumber(int[] nums, int target)
{
for (int i = 0; i < nums.Length; i++)
{
if (nums[i] == target)
{
return ref nums[i]; // Return a reference to the array element
}
}
throw new InvalidOperationException("Number not found");
}
void UpdateNumber()
{
ref int numRef = ref FindNumber(numbers, 3); numRef = 10; // This modifies the original array
Console.WriteLine(string.Join(", ", numbers));
}
[Result]
1, 2, 10, 4, 5
Using ref locals and returns allows you to avoid unnecessary copying of data and enables yo u to work directly with the original data storage location. This is particularly useful when wor king with large structures or arrays where performance is critical.In the example, ref int is use d to declare a method that returns a reference to an integer. The FindNumber function searc hes through an array and returns a reference to the first occurrence of a target value, allowin g modifications to directly affect the original array. This pattern is useful in scenarios where y ou need to frequently modify or update items in a data structure.
[Trivia]
Prior to C# 7.0, ref returning functions were not supported, limiting the efficiency of methods requiring frequent updates to large structures. With the introduction of ref locals and returns
, C# programs can achieve better performance and lower memory usage when manipulating large data structures.4
Out Variables
Out variables allow methods to return values through parameters.
In C#, the 'out' keyword is used to declare variables right where they are passed as method p arameters, improving readability.
[Code]
using System;
class Program
{
static void Main()
{
// Calling a method with an out parameter
if (TryDivide(10, 2, out double result))
{
Console.WriteLine($"Division Result: {result}");
}
}
static bool TryDivide(double numerator, double denominator, out double result)
{
if (denominator == 0)
{
result = 0;
}
result = numerator / denominator;
return true;
}
}
[Result]
Division Result: 5
In this example, the TryDivide method attempts to perform a division operation. The method uses an out parameter (result) to pass the result of the division back to the caller. If the divisi on is possible (i.e., the denominator is not zero), the method sets result and returns true. If th e division is not possible, it sets result to 0 and returns false. This is particularly useful in scen arios where you need to return more than one value from a method or indicate success or fai lure of the operation.
[Trivia]
Using 'out' parameters can simplify methods that need to return multiple results. It avoids th e need for wrapping results in custom structures or tuples, thus reducing overhead and enha ncing performance in some scenarios.
In Parameter Modifier
The 'in' parameter modifier is used for passing arguments by reference but ensures that the argument is not modified.
Here’s a simple method to demonstrate how the 'in' parameter modifier works, preserving th e immutability of the passed variable.
[Code]
using System;
class Program
{
static void Main()
{
int number = 10;
DisplayInParameter(in number);
}
static void DisplayInParameter(in int readOnlyValue)
{
// Attempting to modify readOnlyValue here would result in a compile-time error Console.WriteLine($"The value is: {readOnlyValue}");
}
}
[Result]
The value is: 10
In this code, the DisplayInParameter method uses the in modifier for its parameter, indicating that the value passed to it should be treated as read-only. This means you cannot modify rea dOnlyValue inside the method. This is very useful for ensuring data integrity when you want t o pass large structures or data without the overhead of copying while still protecting the orig inal data from changes.
[Trivia]
The 'in' modifier not only ensures data integrity but also improves performance when dealin g with large data structures or objects. It allows the method to access the original data direct ly without making a copy, but it guarantees that the data will not be modified, thus providing a safe and efficient way to handle large amounts of data.4
Readonly Members in C#
Readonly members in C# allow you to make individual members of a struct readonly. This in dicates that the member does not modify the state of the struct.
The following example demonstrates a struct with a readonly member method. This method calculates the area of a rectangle without modifying any of the struct's fields.
[Code]
public struct Rectangle
{
public readonly double Length;
public readonly double Width;
public Rectangle(double length, double width)
{
Length = length;
Width = width;
}
public readonly double CalculateArea()
{
return Length * Width;
}
}
[Result]
The code itself does not produce output as it defines a structure and a method. However, if y ou instantiate a Rectangle and call CalculateArea, it will return the area based on the provide d dimensions.
Readonly Keyword: In C#, readonly members are typically used in structs to ensure that the method or property does not modify the state of the struct. This can help to maintain immut ability, which is a valuable property in multi-threaded environments where data consistency i s critical.Structs vs. Classes: Unlike classes, structs in C# are value types and not reference typ es. This means they are usually used for smaller amounts of data. Making members readonly in structs ensures they behave more predictably when copied or passed around in the progra m.Performance Benefits: Using readonly members in structs can lead to performance optimiz ations by the compiler, as it can make certain assumptions about how data is accessed and modified.
[Trivia]
Readonly struct members were introduced in C# 8.0, allowing for more fine-grained control over immutability in value types, which is particularly useful in high-performance applications where avoiding unnecessary data mutations is crucial.
Default Interface Methods in C#
Default interface methods were introduced in C# 8.0 to allow interfaces to define a default i mplementation for members. This enables more flexible API development.
The following code shows an interface IAnimal with a default method Eat. This allows differe nt animal types to override this default behavior or use it as is.
[Code]
public interface IAnimal
{
void MakeSound();
// Default implementation of Eat method
void Eat()
{
Console.WriteLine("This animal eats food.");
}
}
[Result]
No direct output unless the Eat method is invoked on an instance of a class implementing IA nimal. When called, it prints "This animal eats food."
Enhancing Interfaces: Default methods enable interfaces to evolve over time without breakin g existing implementations. If a new method is added to an interface, providing a default im plementation ensures that existing classes implementing this interface do not have to modify their code.Multiple Inheritance: This feature indirectly supports a form of multiple inheritance in C#. Classes can inherit behavior from multiple interfaces, and conflicts can be resolved mo re gracefully.Use Cases: Common use cases include providing common functionality across d ifferent classes that share the same interface without enforcing class hierarchy, and offering o ptional methods that can be overridden.
[Trivia]
Default interface methods were added to support richer evolution of libraries, especially APIs like those in .NET Standard and .NET Core, facilitating backward compatibility while adding n ew features.4
Using Declarations
Using declarations in C# automatically dispose of your objects to free up resources.
Below is a simple example of using declarations in C# that shows how they can be used to m anage resources efficiently.
[Code]
static void Main(string[] args)
{
using var file = new System.IO.StreamWriter("example.txt"); file.WriteLine("Hello, C#!");
}
[Result]
The file "example.txt" is created and contains the text "Hello, C#!"
In C#, the using statement is traditionally used to ensure that resources are cleaned up prope rly when objects that implement the IDisposable interface go out of scope. This makes sure t hat resources like file handles, database connections, etc., are released promptly. The traditio nal using statement requires braces {} to define a scope. However, with C# 8.0, a new syntax
called "using declarations" was introduced. This newer syntax automatically disposes of the r esource at the end of the enclosing scope in which the variable is declared, thereby reducing the need for additional braces and making the code cleaner and more readable. It's especiall y useful in reducing nesting and improving code clarity.
[Trivia]
Using declarations are particularly helpful in scenarios involving multiple resources, as each r esource can be declared with its own using statement without the need for nested scopes. T
his simplifies the management of multiple resources, reducing the chance of accidentally mis sing the disposal of any resource.
Switch Expressions
Switch expressions provide a concise way to execute different actions based on different case s.
Here’s an example demonstrating how to use switch expressions in C# to handle multiple co nditions efficiently.
[Code]
static string GetDayMessage(DayOfWeek day) => day switch
{
DayOfWeek.Monday => "Start of a new week!", DayOfWeek.Tuesday => "It's just Tuesday.", DayOfWeek.Wednesday => "Halfway there!",
DayOfWeek.Thursday => "Almost Friday!",
DayOfWeek.Friday => "Weekend is near!",
DayOfWeek.Saturday => "Weekend!",
DayOfWeek.Sunday => "Rest day!",
_ => "Unknown day."
};
[Result]
Calling GetDayMessage(DayOfWeek.Monday) would return "Start of a new week!"
Switch expressions introduced in C# 8.0 offer a more succinct syntax for switch statements, w hich are typically used for branching based on multiple conditions. Each case in a switch expr ession is followed by =>, which clearly denotes the result corresponding to the matched cas e. This differs from the traditional switch statement that uses : and requires break statements.
Switch expressions also ensure that all possible values are handled (either by specific cases or via a discard pattern _), which helps prevent runtime errors due to unhandled cases. This mak es your code more robust and easier to maintain.
[Trivia]
The introduction of switch expressions simplifies functional-style programming in C#. By retu rning values directly through expressions, developers can write more declarative and less err or-prone code. This feature is especially useful in situations where the output of a switch stat ement needs to be immediately returned or used in further computations.4
C# Records
Records in C# are a way to define immutable data types easily.
Here is a simple example of defining a record in C# to represent a person with a name and a ge.
[Code]
public record Person(string Name, int Age);
[Result]
This code does not produce runtime output as it defines a type.
Records in C# are a special kind of class designed for storing immutable data. They provide b uilt-in functionality for value-based equality checks, meaning two record instances are consid ered equal if all their properties have equal values, not just if they are the same instance. This is particularly useful in functional programming styles where immutability and equality based on value rather than object identity are preferred.Records simplify the creation of immutable data models and automatically implement methods like Equals(Object), GetHashCode(), and ToString(), along with others depending on the scenario. They also support with-expressions, which make non-destructive mutation more straightforward. For instance, you can create a n
ew record instance by copying properties from an existing record while changing some of th e properties.
[Trivia]
C# records are implemented using a compiler feature called "record classes", introduced in C
# 9.0 as part of .NET 5.0. This feature provides a concise syntax for defining immutable data t ypes and was designed to improve the development experience when working with data-cen tric applications, such as those commonly found in functional programming or services that r ely heavily on data transfer objects (DTOs).
Init Only Setters
Init-only setters in C# allow properties to be made immutable after object construction.
Below is an example of how to use init-only setters in a class to define immutable properties.
[Code]
public class Product
{
public string Name { get; init; }
public decimal Price { get; init; }
public Product(string name, decimal price)
{
Name = name;
Price = price;
}
}
// Usage
var product = new Product("Laptop", 999.99m);
// product.Name = "Tablet"; // This line would cause a compile-time error because Name is an init-only property.
[Result]
When attempting to modify an init-only property after construction (as in the commented lin e), a compile-time error occurs.
Init-only setters are part of the property definition in C# classes that allow properties to be m utable during the construction phase but become read-only once the object's construction is complete. This feature was introduced in C# 9.0 to enhance the immutability support, especi ally useful in scenarios where data should not change after it's fully initialized, such as with c onfiguration settings or dependency injected services.Using init-only setters, developers can create more robust and secure APIs by enforcing immutability without restricting the usabilit y during object creation. This is particularly helpful in high-concurrency environments or in a pplications requiring high data integrity.
[Trivia]
Init-only setters leverage the new init accessor, which is a variant of the set accessor. The init keyword specifies that the property can only be set during object initialization and is part of t he broader efforts in C# to support more functional programming practices, ensuring safer a nd more predictable code behaviors.4
Top-level Statements
Simplifies program entry point.
In traditional C# programs, the Main method serves as the entry point. Top-level statements reduce this boilerplate by allowing you to write the main logic directly at the top level of the file.
[Code]
// A simple C# application using top-level statements
Console.WriteLine("Hello, World!");
[Result]
Hello, World!
Top-level statements in C# provide a streamlined way to set up a program's entry point. Inst ead of wrapping the entry logic in a Main method within a Program class, you can directly wr ite the executable code at the top level of your file. This feature, introduced in C# 9.0, is parti cularly useful for small projects, scripts, or teaching purposes where the overhead of the tradi tional structure is unnecessary. It simplifies code, reduces indentation, and makes the progra m more approachable to newcomers who may be intimidated by extra boilerplate code.
[Trivia]
Top-level statements are syntactic sugar compiled into a Main method by the C# compiler in a hidden class named <Program>$. This means that while the code looks simpler, the underl ying execution model remains consistent with older C# versions.
Global Using Directives
Streamlines namespace imports.
Global using directives allow you to declare namespace imports that are universally available across all files in a project.
[Code]
// Example of a global using directive in C#
global using System;
Console.WriteLine("Global using makes this work!");
[Result]
Global using makes this work!
Global using directives, introduced in C# 10.0, enable developers to specify using statements that are applied throughout the entire project. This is particularly beneficial in large projects where many files require the same namespaces. By declaring these namespaces once in a ce ntral file (often GlobalUsings.cs or similar), you reduce redundancy and clutter in each source file. It helps in maintaining cleaner code and makes it easier to manage changes to namespa
ce dependencies across a project. It's also a boon for template or library development where consistent namespace usage is critical.
[Trivia]
When you use a global using directive, it's added to the global:: namespace, ensuring it's avai lable across all files without further specification. This feature can be controlled and scoped b y project settings, allowing teams to enforce coding standards and manage dependencies eff ectively.4
File-scoped Namespace Declaration
C# introduced file-scoped namespaces to simplify namespace declaration.
Here is an example of using a file-scoped namespace in C#.
[Code]
namespace ExampleNamespace;
public class ExampleClass
{
public void ExampleMethod()
{
Console.WriteLine("Hello, World!");
}
}
[Result]
Hello, World!
File-scoped namespaces allow the entire file to be encompassed within a single namespace without the need for additional indentation. This feature was introduced in C# 10 and is parti
cularly useful for reducing nesting and improving the readability of the code. Instead of wrap ping everything inside a namespace block, you simply declare the namespace at the top of t he file followed by a semicolon. All subsequent type declarations in that file will automaticall y be part of the declared namespace. This also helps in cleaning up code by removing one le vel of nesting, thus making it less complex and easier to maintain.
[Trivia]
File-scoped namespace declaration is not just a syntactic sugar; it enforces a cleaner and mor e organized structure in large code bases where multiple files often reside in the same names pace. This practice encourages better file organization and code readability.
Nullable Reference Types
Nullable reference types help manage null values in reference types more explicitly.
Below is an example demonstrating how to use nullable reference types in C#.
[Code]
#nullable enable
public class Person
{
public string Name { get; set; }
public string? MiddleName { get; set; } // Nullable reference type public Person(string name, string? middleName = null)
{
Name = name;
MiddleName = middleName;
}
public void PrintName()
{
Console.WriteLine($"Name: {Name}, Middle Name: {MiddleName ?? "[None]"}");
}
}
public static void Main()
{
Person person = new Person("John", "Doe"); person.PrintName();
Person person2 = new Person("Jane");
person2.PrintName();
}
[Result]
Name: John, Middle Name: Doe
Name: Jane, Middle Name: [None]
The nullable reference types feature was introduced in C# 8.0 as a way to provide better safe ty for handling nulls in reference types, which traditionally were allowed to be null by default.
By using nullable annotations (string?), developers can explicitly declare whether a reference type is expected to handle null values or not. Enabling nullable reference types (#nullable en able) raises compiler warnings if potentially null objects are dereferenced without checks, thu s encouraging more robust code. It helps in reducing the occurrence of runtime null referenc e exceptions, which are common sources of bugs in applications.
[Trivia]
Utilizing nullable reference types effectively can greatly reduce the number of null reference exceptions, a common runtime error, thus making the codebase much safer and more reliabl e. This feature represents a significant step towards making C# a safer, more robust language for large-scale development.4
C# 10.0 - Record structs
Record structs are a feature introduced in C# 10.0 to enable value-based equality checks and immutability in a structure similar to record classes.
Here we will define a simple record struct and demonstrate its use in ensuring value-based c omparisons.
[Code]
public record struct Point(int X, int Y);
public class Program
{
public static void Main()
{
var point1 = new Point(1, 2);
var point2 = new Point(1, 2);
var point3 = new Point(3, 4);
Console.WriteLine(point1 == point2); // True, because their values are the same Console.WriteLine(point1 == point3); // False, because their values are different
}
}
[Result]
True
False
In C#, record structs provide a mechanism to handle structures (value types) with value-base d equality rather than reference-based equality, which is typical in objects. This makes record structs particularly useful in scenarios where immutable data types are required, such as in co ncurrent programming or when working with data that shouldn't change once created. The e xample above highlights how two instances of Point are considered equal if their properties (
X and Y) match, showcasing the built-in value-based equality comparison of record structs.
[Trivia]
record struct in C# 10.0 supports other features like with-expressions for non-destructive mu tation. This allows creating a new record struct instance by copying an existing instance and changing some of its properties, while the original instance remains unchanged.
C# 10.0 - Extended Property Patterns
Extended property patterns allow more concise and readable patterns by supporting deeper access paths in pattern matching.
Here, we'll use extended property patterns to simplify condition checks in a nested object str ucture.
[Code]
public class Address
{
public string Country { get; set; }
public string City { get; set; }
}
public class Person
{
public Address Address { get; set; }
}
public class Program
{
public static void Main()
{
var person = new Person
{
Address = new Address { Country = "Japan", City = "Tokyo" }
};
// Using extended property patterns to check properties deep in the object graph if (person is { Address.Country: "Japan", Address.City: "Tokyo" })
{
Console.WriteLine("The person lives in Tokyo, Japan.");
}
}
}
[Result]
The person lives in Tokyo, Japan.
Extended property patterns in C# 10.0 extend the capabilities of pattern matching by allowin g you to specify nested object properties directly in the pattern, reducing the need for multip le is and and checks. This simplifies the code and improves readability, especially when dealin g with complex object hierarchies. In the provided example, the if statement checks if a Perso n object's nested Address object matches certain criteria regarding the Country and City field s using a single concise pattern.
[Trivia]
Extended property patterns enhance the overall efficiency and expressiveness of code, partic ularly in scenarios involving detailed data querying and manipulation within nested object str uctures. They represent a significant step towards more declarative code style in C#.4
Natural Type Expressions in C# 10.0
Natural type expressions simplify variable declarations by using the var keyword, enhancing c ode readability and maintenance.
The example below demonstrates how var can be used with new expressions, eliminating the need to explicitly specify the type on the right-hand side of assignments.
[Code]
var point = new(3, 4);
[Result]
No output, as this is a declaration.
In C# 10.0, when using the new() expression with the var keyword, the type of the variable po int is inferred from the constructor arguments. Here, it is inferred as Point if a constructor Poi nt(int, int) exists. This feature reduces redundancy in code, making it cleaner and easier to ma nage. It's particularly useful when the type name is long or complex, reducing visual clutter.
[Trivia]
Prior to C# 9, each use of new required a full specification of the type. The introduction of tar get-typed new expressions in C# 9 and further enhancements in C# 10 have streamlined obj ect instantiation significantly.
Global Using Directives in C# 10.0
Global using directives allow the declaration of using statements that are universally applicab le across all files in a project, simplifying codebases and reducing repetitive code.
The example below illustrates how to declare a global using directive in a common project fil e.
[Code]
global using System;
global using System.Collections.Generic;
[Result]
No output, as this is a declaration.
Global using directives must be declared at the top of a file, typically in one central file, often named Usings.cs or GlobalUsings.cs in larger projects. Once declared, the namespaces specifi ed are automatically imported into every other file within the same project. This feature redu ces the need to repeatedly declare common using statements in every file, making the code cleaner and reducing the chance of missing namespace errors.
[Trivia]
This feature was introduced in C# 10 to support cleaner and more maintainable codebases, e specially in large projects. It also aligns with the trend in software development towards redu cing boilerplate code and focusing on business logic.4
File-scoped Namespace Enhancement in C# 10.0
C# 10 introduced a feature that allows namespaces to be declared with a simpler syntax that applies to the entire file.
The example below demonstrates the traditional namespace declaration versus the file-scop ed namespace declaration.
[Code]
// Traditional namespace declaration
namespace TraditionalNamespace
{
class Program
{
static void Main(string[] args)
{
System.Console.WriteLine("Hello World using traditional namespace!");
}
}
}
// File-scoped namespace declaration
namespace FileScopedNamespace;
class Program
{
static void Main(string[] args)
{
System.Console.WriteLine("Hello World using file-scoped namespace!");
}
}
[Result]
Hello World using traditional namespace!
Hello World using file-scoped namespace!
The file-scoped namespace feature simplifies code by reducing the level of indentation and making the file easier to read. With this syntax, the entire file is implicitly considered within t he declared namespace. This is particularly useful in projects with many files, each containing code that logically belongs only to a single namespace. It helps to maintain cleaner and mor e maintainable code structures.
[Trivia]
Before C# 10.0, developers had to wrap all the code inside a namespace block, which increas ed indentation and sometimes made code less readable. With the file-scoped namespace, th e code block is directly placed under the namespace declaration, without additional indentati on or braces, thus simplifying the file structure.
List Patterns in C# 11.0
C# 11 introduced list patterns, allowing for more intuitive and declarative list or array content matching in pattern matching contexts.
The following example showcases how to use list patterns to match specific elements in an ar ray.
[Code]
int[] numbers = { 1, 2, 3, 4, 5 };
bool Is123First(int[] nums) => nums is [1, 2, 3, ..]; bool Is345Last(int[] nums) => nums is [.., 3, 4, 5]; System.Console.WriteLine($"Does the array start with 1, 2, 3? {Is123First(numbers)}"); System.Console.WriteLine($"Does the array end with 3, 4, 5? {Is345Last(numbers)}");
[Result]
Does the array start with 1, 2, 3? True
Does the array end with 3, 4, 5? True
List patterns enable developers to check for the presence of a subsequence at the start or en d of a list or array, or even check for specific elements in specific positions, using a concise sy ntax. The .. symbol in the patterns represents "the rest of the list," which can be at the beginn ing, middle, or end of the pattern, offering flexibility in matching list portions. This feature sig nificantly enhances the power of pattern matching in C#, making code that deals with data c ollections much more expressive and easier to understand.
[Trivia]
This feature builds upon and extends the capabilities of positional patterns introduced in earl ier versions of C#. It provides a powerful tool for developers to write more declarative, reada ble, and concise code when working with collections.4
C# 11.0 - Required Properties
Required properties in C# 11.0 ensure that certain properties must be initialized when an obj ect is created, enhancing code robustness by preventing null reference errors.
Here, we define a simple class Person with required properties. These properties must be initi alized when an instance of the class is created.
[Code]
public class Person
{
public required string FirstName { get; set; }
public required string LastName { get; set; }
}
public class Program
{
public static void Main()
{
var person = new Person { FirstName = "John", LastName = "Doe" }; Console.WriteLine($"Name: {person.FirstName} {person.LastName}");
}
}
Name: John Doe
The required keyword is used before the property type to declare that the property must be i nitialized. If a required property is not initialized, the compiler throws an error, which is caug ht at compile time rather than at runtime. This helps in preventing potential bugs related to null values. The properties are initialized directly within the object initializer block when creati ng the Person instance.
[Trivia]
This feature is particularly useful in applications where data integrity is critical, such as in data base transactions and data processing tasks, ensuring that all necessary data elements are pr esent and correctly initialized.
C# 11.0 - Raw String Literals
Raw string literals in C# 11.0 simplify the inclusion of complex strings directly in the code, suc h as JSON, XML, or file paths, without needing to escape special characters.
In this example, we use a raw string literal to handle a JSON snippet directly in the code.
[Code]
public class Program
{
public static void Main()
{
string json = """
{
"name": "John",
"age": 30,
"city": "New York"
}
""";
Console.WriteLine(json);
}
}
{
"name": "John",
"age": 30,
"city": "New York"
}
Raw string literals are delimited by three double quotation marks ("""). Everything between t he delimiters is considered part of the string, including new lines and special characters, with out the need for escaping them. This makes it easier to work with strings that span multiple li nes or contain special characters, like JSON or XML.
[Trivia]
Using raw string literals is extremely beneficial for developers working with large blocks of te xt or code within strings, such as SQL queries, JSON objects, or even embedded HTML, as it i mproves readability and maintainability of the code.4
UTF-8 String Literals in C# 11.0
C# 11 introduces UTF-8 string literals to efficiently handle UTF-8 encoded text.
Below is an example showcasing how to declare and use UTF-8 string literals in C#.
[Code]
string utf8String = "This is a UTF-8 string"u8; Console.WriteLine(utf8String);
[Result]
This is a UTF-8 string
In C# 11.0, UTF-8 string literals allow for the direct creation of strings encoded in UTF-8, whic h can be more memory efficient, particularly for applications dealing with a lot of non-ASCII characters. These literals are denoted by appending u8 to the end of the string. This feature i s particularly useful in scenarios where text processing with UTF-8 encoding is essential, such as web applications and file I/O operations that require UTF-8. It simplifies handling UTF-8 d ata without the need for explicit encoding conversions, thus reducing the likelihood of encod ing errors and improving performance.
[Trivia]
Before C# 11.0, handling UTF-8 encoded strings in C# could involve additional steps such as converting to and from System.Text.Encoding.UTF8. This new feature simplifies interactions w ith systems and libraries that default to UTF-8, aligning C# more closely with web standards a nd modern application development practices.
Enhanced #line Directive in C# 11.0
C# 11 enhances the #line directive for more flexible control over line numbers and file paths i n error messages and debugging.
Here's an example demonstrating the use of the enhanced #line directive to modify the file p ath and line numbers in compiler messages.
[Code]
#line 200 "SpecialFile.cs"
Console.WriteLine("Hello from a modified line number and file!");
#line default
Console.WriteLine("Back to original line numbers.");
[Result]
Hello from a modified line number and file!
Back to original line numbers.
The enhanced #line directive in C# 11.0 offers improved capabilities for developers to manip ulate the line numbers and file paths that appear in compiler errors and debug information.
This can be particularly useful for generating code (e.g., code generated by tools or libraries), where you might want to redirect error messages to the original source code instead of the g enerated code. This makes debugging easier and more intuitive. By specifying a custom file n ame and line number, developers can make the error messages more meaningful and tailore d to their project's structure.
[Trivia]
The #line directive has been part of C# since its inception but was limited to basic line numb er manipulation. The enhancements in C# 11.0 introduce more granular control, aiding in de bugging scenarios where the physical source code differs from the original source layout, suc h as in generated or transformed code. This is especially useful in large projects with comple x build processes or where multiple preprocessing steps are involved.4
C# Compiler (Roslyn)
The Roslyn compiler is a set of open-source compilers and code analysis APIs for C#.
Here's an example of compiling a simple C# program using the Roslyn csc command.
[Code]
// HelloWorld.cs
using System;
class Program
{
static void Main()
{
Console.WriteLine("Hello, World!");
}
}
Copy codecsc HelloWorld.cs
[Result]
HelloWorld.exe
The csc command is the C# compiler command used to compile C# programs into executabl e files (.exe) or libraries (.dll). The provided code example defines a basic C# program that pri nts "Hello, World!" to the console. To compile this, you run the csc HelloWorld.cs command i n the command prompt where csc is the C# compiler executable and HelloWorld.cs is the file containing the C# source code. The compilation process reads the C# code, processes it, and creates an executable file (HelloWorld.exe) which, when run, will output "Hello, World!" to th e console.
[Trivia]
Roslyn provides rich APIs for analyzing and manipulating code, making it a powerful tool for developers creating IDEs, refactoring tools, or static analysis tools. It's also used extensively w ithin Visual Studio to provide code suggestions and refactoring.
.NET Runtime
The .NET Runtime is the execution environment that runs .NET applications, managing memo ry, execution, and more.
Below is an example demonstrating the execution of a C# program that calculates and displa ys the result of a simple arithmetic operation.
[Code]
// SimpleCalculation.cs
using System;
class Program
{
static void Main()
{
int a = 5;
int b = 3;
int sum = a + b;
Console.WriteLine($"The sum of {a} and {b} is {sum}");
}
}
dotnet run SimpleCalculation.cs
The sum of 5 and 3 is 8
The dotnet run command is used to build and execute a C# program in a development envir onment. It compiles the program using the .NET CLI (Command Line Interface), automatically determines the dependencies, and then runs the executable. This command is part of the .NE
T SDK and is particularly useful for quickly testing and debugging applications during develo pment. The code provided defines two integer variables, a and b, adds them together, and th en prints the result. This simple example illustrates basic arithmetic operations and console o utput in C#.
[Trivia]
The .NET Runtime, also known as CLR (Common Language Runtime), provides services like g arbage collection, exception handling, and type safety, making it a robust environment for ex ecuting a wide range of applications. It supports multiple languages, allowing them to use th e same base libraries and to interoperate.4
Entity Framework Basics
Entity Framework (EF) is an Object-Relational Mapping (ORM) framework for .NET. It enables developers to work with data using objects of domain-specific classes without focusing on th e underlying database tables and columns where this data is stored.
The following example demonstrates how to create a simple model representing a "Blog" an d how to use Entity Framework to add a new Blog to the database.
[Code]
using System;
using Microsoft.EntityFrameworkCore;
public class BloggingContext : DbContext
{
public DbSet<Blog> Blogs { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlServer(@"Server=(localdb)\\mssqllocaldb;Database=Blogging;Tru sted_Connection=True;");
}
}
public class Blog
{
public int BlogId { get; set; }
public string Url { get; set; }
}
class Program
{
static void Main()
{
using (var db = new BloggingContext())
{
var blog = new Blog { Url = "http://sampleblog.com" }; db.Blogs.Add(blog);
db.SaveChanges();
Console.WriteLine($"Blog added with ID: {blog.BlogId}");
}
}
}