Ticker

6/recent/ticker-posts

Header Ads Widget

The Essential C#/.NET Handbook # 80 -85

 

The Essential C#/.NET Handbook # 80 -85 - Encyclopedia of C-Sharp Notions

80. implicit operator

    An implicit operator in C# defines automatic type conversions between user-defined types and other types without requiring an explicit cast. It prevents unnecessary casting syntax while ensuring type safety.

To define an implicit conversion, use the implicit keyword inside a class:

public class Temperature

{

    public double Celsius { get; }

 

    public Temperature(double celsius) { Celsius = celsius; }

 

    public static implicit operator double(Temperature temp) => temp.Celsius;

 

    public static implicit operator Temperature(double value) => new Temperature(value);

}

Usage:

Temperature temp = 37.5;  // Automatically converts from double to Temperature

double degrees = temp;  // Automatically converts from Temperature to double

Unlike explicit conversions (explicit operator), implicit operators avoid casting in the user’s code, making conversions seamless while preventing data loss.

Implicit operators are useful when:

  • Converting between compatible types (int ↔ Fraction, double ↔ Currency).
  • Simplifying object creation from primitive types.
  • Avoiding redundant .ToX() or casting ((T)x) in code.

However, implicit conversions should be used with caution to avoid unintended conversions, especially when data loss is possible. When conversions are not always safe, explicit operators are preferred.

 

81. explicit operator

    An explicit operator in C# defines a custom type conversion that requires an explicit cast ((T)) in user code. Unlike an implicit operator, which allows automatic conversion, an explicit operator ensures that the developer consciously acknowledges the conversion, typically when there’s a risk of data loss.

To define an explicit conversion, use the explicit keyword inside a class:

public class Temperature

{

    public double Celsius { get; }

 

    public Temperature(double celsius) { Celsius = celsius; }

 

    public static explicit operator int(Temperature temp) => (int)temp.Celsius;

}

public class Temperature

{

    public double Celsius { get; }

 

    public Temperature(double celsius) { Celsius = celsius; }

 

    public static explicit operator int(Temperature temp) => (int)temp.Celsius;

}

Usage:

Temperature temp = new Temperature(36.7);

int roundedTemp = (int)temp; // Explicit cast required

This prevents unintended truncation of decimal values.

Explicit operators are ideal when conversions are not always safe—for example, converting a double to an int may result in loss of precision. They provide a safeguard by forcing the developer to use an explicit cast, ensuring intentional conversion.

While explicit conversions enhance clarity, excessive reliance on them can lead to unreadable code. Use them when necessary, but favor safer alternatives when possible.

 

82. conversion operator

    A conversion operator in C# defines how a custom type can be converted to or from another type. There are two types:

  • Implicit conversion (implicit operator) – Automatic, safe conversion (no data loss).
  • Explicit conversion (explicit operator) – Requires an explicit cast to prevent unintended data loss.

Example:

public class Currency

{

    public double Amount { get; }

    public Currency(double amount) { Amount = amount; }

 

    // Implicit conversion from double to Currency

    public static implicit operator Currency(double value) => new Currency(value);

 

    // Explicit conversion from Currency to int

    public static explicit operator int(Currency c) => (int)c.Amount;

}

Usage:

Currency money = 50.75;  // Implicit conversion

int roundedMoney = (int)money;  // Explicit conversion required

Conversion operators improve code readability by reducing redundant .ToX() methods. They are commonly used in mathematical, unit-based, or custom data structures where seamless type interoperability is needed.

However, avoid overloading too many conversions, as it can make the code harder to understand. Always prioritize clarity and safety when defining conversion rules.

 

83. indexer

    An indexer in C# allows objects to be accessed using array-like syntax ([]). It is useful when designing classes that act like collections but are not necessarily arrays or lists.

Indexers are defined using the this keyword:

public class Week

{

    private string[] days = { "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" };

 

    public string this[int index]

    {

        get => days[index];

        set => days[index] = value;

    }

}

Usage:

Week week = new Week();

Console.WriteLine(week[0]); // Output: Sunday

week[0] = "Funday";

Console.WriteLine(week[0]); // Output: Funday

Key Features:
Provides array-like access to objects.
Supports read-only (get) or modifiable (get and set) properties.
Can be overloaded with multiple parameters (this[int, int]).
Commonly used in custom collections, matrices, or structured data storage.

Indexers improve code readability by making data retrieval intuitive while keeping the internal structure encapsulated. They allow developers to customize how elements are accessed while enforcing logic inside the get/set blocks.

 

84. pointer types

    Pointer types in C# allow direct memory manipulation but are only permitted in unsafe code blocks. Unlike references (object, class), pointers store memory addresses directly, similar to C/C++.

Pointer syntax:

unsafe

{

    int number = 10;

    int* ptr = &number; // Pointer stores the address of 'number'

    Console.WriteLine(*ptr); // Dereferencing pointer to get value

}

Key Rules for Pointers:
Declared using * (e.g., int* p).
Can store the memory address of a variable (&var).
Can be dereferenced using *ptr to access the value.
Used with structs, arrays, and interop with unmanaged code.
Only allowed in unsafe blocks and require /unsafe compiler option.

Common pointer types:

  • int*, double*, char* → Stores the address of primitive types.
  • void* → Generic pointer without a specific type.
  • T* → Used for unmanaged struct types.

Pointer arithmetic is also allowed:

unsafe

{

    int arr[] = {1, 2, 3};

    fixed (int* ptr = arr)

    {

        Console.WriteLine(*(ptr + 1)); // Access second element

    }

}

While powerful, pointers bypass .NET’s memory safety model and can lead to issues like memory leaks or buffer overflows. They are primarily used in performance-critical applications, system programming, or interop with unmanaged code (e.g., working with C libraries).

For most applications, safe alternatives like Span<T>, Memory<T>, or IntPtr should be preferred unless raw memory access is necessary.

 

85. unsafe code

    Unsafe code in C# allows direct memory manipulation using pointers, bypassing the .NET runtime's safety features like garbage collection and type checking. It is primarily used for interacting with unmanaged code, improving performance, or working with low-level system resources.

To enable unsafe code:

  1. Use the unsafe keyword in a block, method, or class.
  2. Enable the /unsafe compiler option.

Example:

unsafe

{

    int number = 42;

    int* ptr = &number; // Pointer stores memory address of 'number'

    Console.WriteLine(*ptr); // Dereference pointer (prints 42)

}

Common Use Cases:

  • Interacting with native libraries (DLLImport, PInvoke).
  • Performance optimization (avoiding bounds checking in arrays).
  • Memory management (allocating/freeing memory manually).

Fixed Pointers with Structs:

unsafe

{

    fixed (char* ptr = "Hello")

    {

        Console.WriteLine(ptr[0]); // Prints 'H'

    }

}

Unsafe Code in Structs:

unsafe struct MyStruct

{

    public fixed int Numbers[5]; // Fixed-size array inside a struct

}

🚨 Risks of Unsafe Code:
Bypasses type safety → Risk of buffer overflows and memory corruption.
Manual memory management → Can cause memory leaks.
Harder to debug → No automatic garbage collection for unsafe operations.

🔹 Alternatives:
In most cases, .NET provides safer alternatives like:
Span<T> and Memory<T> (for efficient, safe memory access).
IntPtr (for safe pointer-like functionality).
Marshal class (for managed-unmanaged interop).

Use unsafe code only when absolutely necessary, ensuring it is well-tested and optimized for performance-critical tasks like game engines, device drivers, or high-performance computing.