Skip to main content

Command Palette

Search for a command to run...

Mastering C# Part 2.2 - Fundamentals

Access Modifiers, Operators, Params, Type Casting, Enumeration, Structs, Nullable, Conditional statements, Jump Statements

Updated
13 min read
Mastering C# Part 2.2 - Fundamentals

Now, let’s learn about Access Modifiers in C#

Access Modifiers

Access modifiers in C# define how accessible a member (e.g., a field, method, or class) is to other parts of the program. Here’s a detailed breakdown of each access modifier based on the table you provided:


1. Public

  • Scope: Entire program.

  • Usage: Members marked as public can be accessed from anywhere in the program or even from another assembly.

  • Example:

      public class Example
      {
          public int PublicValue = 10; // Accessible anywhere
      }
    

2. Private

  • Scope: Containing class.

  • Usage: Members marked as private are accessible only within the same class where they are declared.

  • Example:

      class Example
      {
          private int PrivateValue = 20; // Accessible only inside this class
    
          void ShowValue()
          {
              Console.WriteLine(PrivateValue); // Valid
          }
      }
    

3. Protected

  • Scope: Containing class and derived types.

  • Usage: Members marked as protected are accessible within the same class or in derived classes.

  • Example:

      class Parent
      {
          protected int ProtectedValue = 30; // Accessible in derived classes
      }
    
      class Child : Parent
      {
          void ShowValue()
          {
              Console.WriteLine(ProtectedValue); // Valid
          }
      }
    

4. Internal

  • Scope: Current assembly.

  • Usage: Members marked as internal are accessible within the same assembly, Derived class but not from other assemblies.

  • Example:

      internal class Example
      {
          internal int InternalValue = 40; // Accessible within the same assembly
      }
    

5. Protected Internal

  • Scope: Derived types or within the current assembly.

  • Usage: Members marked as protected internal are accessible to:

    • Derived classes (even in different assemblies).

    • Any code within the same assembly.

  • Example:

      class Parent
      {
          protected internal int ProtectedInternalValue = 50;
      }
    
      class Child : Parent
      {
          void ShowValue()
          {
              Console.WriteLine(ProtectedInternalValue); // Valid
          }
      }
    

6. Private Protected

  • Scope: Derived types within the current assembly.

  • Usage: Members marked as private protected are accessible:

    • Only in derived classes within the same assembly.

    • Not accessible outside the assembly, even by derived classes.

  • Example:

      class Parent
      {
          private protected int PrivateProtectedValue = 60;
      }
    
      class Child : Parent
      {
          void ShowValue()
          {
              Console.WriteLine(PrivateProtectedValue); // Valid within same assembly
          }
      }
    

Summary Table

ModifierEntire ProgramContaining ClassCurrent AssemblyDerived TypesDerived Types (Same Assembly)
public
protected
internal
protected internal
private
private protected

In C#, assemblies are the building blocks of .NET applications. An assembly is essentially a compiled output of your code, typically in the form of a .dll (Dynamic Link Library) or .exe (Executable).

Operators | Params | Type Casting

Operators in C# are essential for performing computations and logic operations on variables and values (called operands). Below is an overview of the key operator categories in C#:


1. Arithmetic Operators

These are used for mathematical operations:

OperatorDescriptionExample
+Additiona + b
-Subtractiona - b
*Multiplicationa * b
/Divisiona / b
%Modulus (remainder)a % b

2. Relational Operators

Used to compare values and return true or false:

OperatorDescriptionExample
==Equal toa == b
!=Not equal toa != b
>Greater thana > b
<Less thana < b
>=Greater than or equal toa >= b
<=Less than or equal toa <= b

3. Logical Operators

Used to combine conditions:

OperatorDescriptionExample
&&Logical AND(a > b) && (b > c)
``
!Logical NOT!(a > b)

4. Bitwise Operators

Used for bit-level operations:

OperatorDescriptionExample
&ANDa & b
``OR
^XORa ^ b
~Complement (NOT)~a
<<Left shifta << 2
>>Right shifta >> 2

5. Assignment Operators

Used to assign values to variables:

OperatorDescriptionExample
=Assigna = 10
+=Add and assigna += b
-=Subtract and assigna -= b
*=Multiply and assigna *= b
/=Divide and assigna /= b
%=Modulus and assigna %= b

6. Conditional (Ternary) Operator

Shorthand for an if-else statement:

OperatorDescriptionExample
? :Conditionresult = (a > b) ? a : b;

Example Code

using System;

class Program
{
    static void Main()
    {
        int a = 10, b = 5, c = 0;

        // Arithmetic Operators
        Console.WriteLine("Arithmetic Operations:");
        Console.WriteLine($"Addition (a + b): {a + b}");         // 15
        Console.WriteLine($"Subtraction (a - b): {a - b}");      // 5
        Console.WriteLine($"Multiplication (a * b): {a * b}");   // 50
        Console.WriteLine($"Division (a / b): {a / b}");         // 2
        Console.WriteLine($"Modulus (a % b): {a % b}");          // 0

        // Relational Operators
        Console.WriteLine("\\nRelational Comparisons:");
        Console.WriteLine($"Is a equal to b? (a == b): {a == b}"); // false
        Console.WriteLine($"Is a not equal to b? (a != b): {a != b}"); // true
        Console.WriteLine($"Is a greater than b? (a > b): {a > b}"); // true

        // Logical Operators
        Console.WriteLine("\\nLogical Operations:");
        Console.WriteLine($"Logical AND (a > 5 && b < 10): {(a > 5 && b < 10)}"); // true
        Console.WriteLine($"Logical OR (a < 5 || b < 10): {(a < 5 || b < 10)}");  // true
        Console.WriteLine($"Logical NOT !(a > b): {! (a > b)}");                 // false

        // Assignment Operators
        Console.WriteLine("\\nAssignment Operators:");
        c = a + b; // Assign sum of a and b to c
        Console.WriteLine($"Assign (c = a + b): {c}"); // 15
        c += b; // Add b to c
        Console.WriteLine($"Add and Assign (c += b): {c}"); // 20
        c *= 2; // Multiply c by 2
        Console.WriteLine($"Multiply and Assign (c *= 2): {c}"); // 40

        // Bitwise Operators
        Console.WriteLine("\\nBitwise Operations:");
        Console.WriteLine($"Bitwise AND (a & b): {a & b}"); // 0 (binary AND)
        Console.WriteLine($"Bitwise OR (a | b): {a | b}"); // 15 (binary OR)
        Console.WriteLine($"Bitwise XOR (a ^ b): {a ^ b}"); // 15 (binary XOR)
        Console.WriteLine($"Left Shift (a << 1): {a << 1}"); // 20
        Console.WriteLine($"Right Shift (a >> 1): {a >> 1}"); // 5

        // Conditional (Ternary) Operator
        Console.WriteLine("\\nConditional (Ternary) Operator:");
        string result = (a > b) ? "a is greater" : "b is greater";
        Console.WriteLine($"Result: {result}"); // "a is greater"

        // Null-coalescing Operator (??)
        Console.WriteLine("\\nNull-coalescing Operator:");
        string nullValue = null;
        string nonNullValue = nullValue ?? "Default Value";
        Console.WriteLine($"Null Value: {nonNullValue}"); // "Default Value"

        // Null-coalescing Assignment Operator (??=)
        Console.WriteLine("\\nNull-coalescing Assignment:");
        string nullableString = null;
        nullableString ??= "Assigned Default";
        Console.WriteLine($"Nullable String after ??=: {nullableString}"); // "Assigned Default"

        // Compound Operator Behavior with Pre/Post-Increment
        Console.WriteLine("\\nPre/Post-Increment Operators:");
        Console.WriteLine($"a: {a}, a++: {a++}, a after increment: {a}"); // 10, 10, 11
        Console.WriteLine($"b: {b}, ++b: {++b}, b after increment: {b}"); // 5, 6, 6

        // Tricky: Short-Circuiting with Logical Operators
        Console.WriteLine("\\nLogical Short-Circuiting:");
        int x = 0;
        bool shortCircuit = (x > 10) && (++x > 0); // x is not incremented because (x > 10) is false
        Console.WriteLine($"Value of x after short-circuit: {x}"); // 0

        // Tricky: Overflow Handling
        Console.WriteLine("\\nOverflow Handling:");
        int maxInt = int.MaxValue;
        unchecked // Overflow is ignored
        {
            int overflowResult = maxInt + 1; // Wraps around to negative
            Console.WriteLine($"Overflow Result (unchecked): {overflowResult}"); // -2147483648
        }
        checked // Overflow throws an exception
        {
            try
            {
                int checkedOverflow = maxInt + 1; // Throws OverflowException
            }
            catch (OverflowException ex)
            {
                Console.WriteLine($"Caught OverflowException: {ex.Message}");
            }
        }

        // Tricky: Operator Precedence
        Console.WriteLine("\\nOperator Precedence:");
        int precedenceResult = a + b * 2; // Multiplication has higher precedence than addition
        Console.WriteLine($"Result of a + b * 2: {precedenceResult}"); // 22
    }
}

More about Operators

Short-Circuiting in Logical Operators

  • && and || operators stop evaluating as soon as the result is determined, which can affect side-effects like variable increments.

Null-Coalescing Operator (??) and Assignment (??=)

  • Used to assign a value only if the left-hand operand is null.

Overflow Behavior

  • checked and unchecked blocks control overflow behavior for arithmetic operations.

  • By default, overflow is unchecked, which can cause silent errors in critical calculations.

Operator Precedence

  • Multiplication, division, and modulus operators (, /, %) have higher precedence than addition and subtraction (`+`, ). Use parentheses to avoid confusion.

Bitwise vs Logical Operators

  • Bitwise operators (&, |) operate at the bit level, while logical operators (&&, ||) evaluate entire boolean expressions.

Params

The params keyword allows a method to accept a variable number of arguments. This is useful when the number of inputs is not known beforehand. It is used with arrays and enables passing multiple arguments without explicitly defining an array.


Important Points About params:

  1. Variable Number of Arguments:

    • The method can accept zero or more arguments of the specified type.

    • If no arguments are passed, the params array will have a length of 0.

  2. Only One params Parameter:

    • A method can have only one params parameter.

    • It must be the last parameter in the method signature.

  3. Works With Arrays:

    • If an array is passed, the params parameter directly references that array.
  4. Default Behavior:

    • If no arguments are provided, the params array is initialized to an empty array.

Examples:

1. Simple Use of params

using System;

class Program
{
    static int Add(params int[] numbers)
    {
        int sum = 0;
        foreach (int num in numbers)
        {
            sum += num;
        }
        return sum;
    }

    static void Main(string[] args)
    {
        Console.WriteLine(Add(1, 2, 3, 4));  // Output: 10
        Console.WriteLine(Add());           // Output: 0
    }
}

2. Passing an Array to params

using System;

class Program
{
    static void PrintNumbers(params int[] numbers)
    {
        foreach (var num in numbers)
        {
            Console.WriteLine(num);
        }
    }

    static void Main(string[] args)
    {
        int[] arr = { 10, 20, 30 };
        PrintNumbers(arr);  // Output: 10, 20, 30
    }
}

3. Using params with Objects

params can also accept a variable number of objects, enabling flexibility with data types.

using System;

class Program
{
    static void Print(params object[] items)
    {
        foreach (var item in items)
        {
            Console.WriteLine(item);
        }
    }

    static void Main(string[] args)
    {
        Print(1, "hello", 3.5, true);
        // Output:
        // 1
        // hello
        // 3.5
        // True
    }
}

Interview-Level Tricky Concepts

1. Method Overloading with params

If a method has overloads, the one with params will only be used when there is no exact match for the arguments.

using System;

class Program
{
    static void Display(int a, int b)
    {
        Console.WriteLine($"Exact match: {a}, {b}");
    }

    static void Display(params int[] numbers)
    {
        Console.WriteLine("Using params:");
        foreach (var num in numbers)
        {
            Console.WriteLine(num);
        }
    }

    static void Main(string[] args)
    {
        Display(1, 2);         // Calls the exact match
        Display(1, 2, 3);      // Calls the params method
    }
}

2. Combining params with Regular Parameters

You can combine params with regular parameters, but the params parameter must come last.

using System;

class Program
{
    static void Print(string prefix, params string[] words)
    {
        Console.WriteLine(prefix);
        foreach (var word in words)
        {
            Console.WriteLine(word);
        }
    }

    static void Main(string[] args)
    {
        Print("Words:", "apple", "banana", "cherry");
        // Output:
        // Words:
        // apple
        // banana
        // cherry
    }
}

3. Passing Null or Empty Arrays

Passing null or no arguments behaves differently. If no arguments are provided, the params array is initialized to an empty array. However, explicitly passing null will set the params parameter to null.

using System;

class Program
{
    static void Print(params int[] numbers)
    {
        if (numbers == null)
        {
            Console.WriteLine("Params is null");
        }
        else
        {
            Console.WriteLine($"Array Length: {numbers.Length}");
        }
    }

    static void Main(string[] args)
    {
        Print();               // Output: Array Length: 0
        Print(null);           // Output: Params is null
    }
}

4. Difference Between Passing Array Directly vs Using params

When passing an array to a method with a params parameter, the method can use it directly, but without params, the method treats it as a single argument.

using System;

class Program
{
    static void PrintNumbers(int[] numbers)
    {
        foreach (var num in numbers)
        {
            Console.WriteLine(num);
        }
    }

    static void Main(string[] args)
    {
        int[] arr = { 1, 2, 3 };

        PrintNumbers(arr);   // Valid
        // PrintNumbers(1, 2, 3); // Error: Requires an array
    }
}

Key Questions Asked in Interviews

  1. Why can’t params be used with multiple parameters in a method?

    It’s because the compiler wouldn’t be able to determine how to match arguments to parameters.

  2. What happens if an array is passed to a method with a params parameter?

    The method directly uses the array without creating a new array.

  3. What is the difference between params and object arrays?

    params simplifies the syntax by allowing multiple arguments, whereas an object[] array requires explicit array initialization.

  4. Can params be used with nullable reference types?

    Yes, you can pass null values, but you should handle them carefully to avoid runtime exceptions.

Type Casting

Type conversion happens when we assign the value of one data type to another. If the data types are compatible, then C# does Automatic Type Conversion. If not comparable, then they need to be converted explicitly which is known as Explicit Type conversion.

Implicit Type Casting / Automatic Type Conversion

It happens when:

  • The two data types are compatible.

  • When we assign value of a smaller data type to a bigger data type.

| From Data Type | To Data Types                     |
|----------------|-----------------------------------|
| byte           | → short, int, long, float, double |
| short          | → int, long, float, double        |
| int            | → long, float, double             |
| long           | → float, double                   |
| float          | → double                          |

If we want to assign a value of larger data type to a smaller data type we perform explicit type casting.

Sometimes, it may result into the lossy conversion.

static void Main(String[] args){

           double d = 345.32; // example for lossy conversion
           int i  = (int)d; // implicit conversion
           Console.WriteLine(i); // 345
           // .32 is lost due to lossy conversion
        }

C# provides built-in methods for Type-Conversions as follows

| Method     | Description                                             |
|------------|---------------------------------------------------------|
| ToBoolean  | Converts a type to a Boolean value                      |
| ToChar     | Converts a type to a character value                    |
| ToByte     | Converts a value to a Byte value                        |
| ToDecimal  | Converts a value to a Decimal point value               |
| ToDouble   | Converts a type to a double data type                   |
| ToInt16    | Converts a type to a 16-bit integer                     |
| ToInt32    | Converts a type to a 32-bit integer                     |
| ToInt64    | Converts a type to a 64-bit integer                     |
| ToString   | Converts a given type to a string                       |
| ToUInt16   | Converts a type to an unsigned 16-bit integer           |
| ToUInt32   | Converts a type to an unsigned 32-bit integer           |
| ToUInt64   | Converts a type to an unsigned 64-bit integer           |

Code Example

static void Main(String[] args){

           double d = 345.32;
           string i  = Convert.ToString(d);
           Console.WriteLine(i); // string
           Console.WriteLine(Convert.ToUInt32(d));// .32 is lost
           Console.WriteLine(i.GetType());// System.String
           Console.WriteLine(Convert.ToDecimal(i)); //345.32


        }