Zero to Hero: Journey of Mastering C# From Scratch part 2

Rashmi Ireshe
11 min readMar 25, 2024

--

Welcome to Part 2 of our Learning C# series. In our previous post, we delved into creating a C# project, variable declarations, operations, conditional statements (if and switch), and loops. If you haven’t checked it out yet, please click here to read Part 1.

Today, we’ll dive deeper into string operations, formatting, interpolation, the immutability of strings, and the power of the StringBuilder class. Additionally, we’ll touch upon parsing strings to other data types, creating functions, and exploring the basics of Object-Oriented Programming (OOP) in C#.

String Operations in C#

C# provides a rich set of operations for managing strings. Let’s start with a basic example demonstrating various string operations:

static void Main(string[] args)
{
// Initialize strings for demonstration
string resultStr;
string sampleText1 = "A quick brown cat leaps over a lazy fox.";
string sampleText2 = "Learning C# is fun";
string sampleText3 = "LEARNING c# IS FUN";
string[] wordsArray = {"apple", "banana", "cherry", "date"};

// Demonstrating the length property of a string
Console.WriteLine(sampleText1.Length);

// Accessing individual characters
Console.WriteLine(sampleText1[10]);

// Iterating over a string like a sequence of characters
foreach (Char letter in sampleText1) {
Console.Write(letter);
if (letter == 'c') {
Console.WriteLine();
break;
}
}

// Demonstrating string concatenation
resultStr = String.Concat(wordsArray);
Console.WriteLine(resultStr);

// Joining strings with a specified separator using Join
resultStr = String.Join(';', wordsArray);
Console.WriteLine(resultStr);
resultStr = String.Join("==", wordsArray);
Console.WriteLine(resultStr);

// String Comparison
// Compare will perform an ordinal comparison and return:
// < 0 : first string comes before second in sort order
// 0 : first and second strings are same position in sort order
// > 0 : first string comes after the second in sort order
int compareResult = String.Compare(sampleText2, "Learning C# is fun");
Console.WriteLine("{0}", compareResult);

// Checking for equality, returns a boolean
bool areEqual = sampleText2.Equals(sampleText3);
Console.WriteLine("{0}", areEqual);

// String searching operations
Console.WriteLine("{0}", sampleText1.IndexOf('a'));
Console.WriteLine("{0}", sampleText1.IndexOf("cat"));

Console.WriteLine("{0}", sampleText1.LastIndexOf('a'));
Console.WriteLine("{0}", sampleText1.LastIndexOf("lazy"));

// Replacing substrings within a string
resultStr = sampleText1.Replace("cat", "squirrel");
Console.WriteLine("{0}", resultStr);
Console.WriteLine("{0}", resultStr.IndexOf("cat")); // Should return -1 as "cat" is no longer present
}

String Formatting and Interpolation

Formatting in C# allows for presenting data in a readable format. String interpolation, introduced in C# 6, enables embedding expressions into a string literal.

static void Main(string[] args)
{
int[] months = {1, 5, 9, 12};
int[] revenues = {120000, 180000, 240000, 300000};
double[] onlineSalesPercentage = {.345, .389, .425, .489};
string sampleText = "FormatDemo";
int numberExample = 4321;
decimal decimalExample = 7654.3210m;

// Displaying basic string information
Console.WriteLine("{0}", sampleText);

// Demonstrating numerical formatting
// The general format structure is {index[,alignment]:[formatSpecifier]}
// Format specifiers include N (Number), G (General), F (Fixed-point),
// E (Exponential), D (Decimal), P (Percent), X (Hexadecimal),
// and C (Currency based on local settings)
Console.WriteLine("{0:D}, {0:N}, {0:F}, {0:G}", numberExample);
Console.WriteLine("{0:E}, {0:N}, {0:F}, {0:G}", decimalExample);

// Specify precision by adding a number after the format specifier
Console.WriteLine("{0:D5}, {0:N3}, {0:F2}, {0:G4}", numberExample);

// Formatting with alignment for better readability
Console.WriteLine("Revenue by Month:");
Console.WriteLine("{0,10} {1,10} {2,10} {3,10}", months[0], months[1], months[2], months[3]);
Console.WriteLine("{0,10:C} {1,10:C} {2,10:C} {3,10:C}", revenues[0], revenues[1], revenues[2], revenues[3]);
Console.WriteLine("Online Sales Ratios:");
Console.WriteLine("{0,10:P} {1,10:P} {2,10:P1} {3,10:P2}", onlineSalesPercentage[0], onlineSalesPercentage[1], onlineSalesPercentage[2], onlineSalesPercentage[3]);
}

Output result

result of string formatting of above code in C#
result of string formatting c# code sample

Example for String interpolation

static void Main(string[] args)
{
// Initialize variables for demonstration
string brand = "Audi";
string type = "Q8";
int manufactureYear = 2022;
float mileage = 5_300.75f; // using underscores for readability
decimal salePrice = 75_500m; // 'm' denotes a decimal literal

// Display car details using standard formatting
Console.WriteLine("Available vehicle: {0} {1} {2}, with {3} miles, offered at ${4}.",
manufactureYear, brand, type, mileage, salePrice);

// Using string interpolation for more concise and readable code
Console.WriteLine($"Available vehicle: {manufactureYear} {brand} {type}, with {mileage} miles, offered at {salePrice:C2}.");

// Including inline expressions within string interpolation for dynamic content
Console.WriteLine($"Available vehicle: {manufactureYear} {brand} {type}, with {mileage * 1.60934:F2} kilometers, offered at {salePrice:C2}.");
}

The Immutability of Strings and StringBuilder

Strings in C# are immutable. Every modification creates a new string, which can lead to performance issues in extensive operations. The StringBuilder class is designed to overcome this limitation by allowing mutable string operations.

static void Main(string[] args)
{
StringBuilder sb = new StringBuilder("Start of Story. ", 250);
int runCount = 5;
string[] creatures = {"deer", "rabbits", "squirrels"};

// Display basic stats about the StringBuilder
Console.WriteLine($"Initial Capacity: {sb.Capacity}; Initial Length: {sb.Length}");

// Adding text to the StringBuilder with Append
sb.Append("A clever little squirrel ");
sb.Append("darts through the forest.");

// AppendLine adds text along with a newline character
sb.AppendLine();

// AppendFormat is useful for adding formatted text
sb.AppendFormat("This happened {0} times today.", runCount);
sb.AppendLine();

// AppendJoin combines a collection of strings with a specified separator
sb.Append("It was seen scurrying past ");
sb.AppendJoin(", ", creatures);

// Using Replace to change text within the StringBuilder
sb.Replace("squirrel", "chipmunk");

// Inserting text at a specific index
sb.Insert(0, "Nature Watch: ");

Console.WriteLine($"Updated Capacity: {sb.Capacity}; Updated Length: {sb.Length}");

// Converting the StringBuilder contents to a single string for output
Console.WriteLine(sb.ToString());
}

Parsing Strings to Other Data Types

Converting string representations of numbers, dates, or other data types is a common requirement. C# provides methods like int.Parse(), bool.Parse(), and TryParse() methods for safe parsing.

static void Main(string[] args)
{
string valueStr1 = "5";
string valueStr2 = "8.25";
string valueStr3 = "4,500";
string valueStr4 = "6,789.01";

// Parsing a string into a number might throw an exception, so it's important to handle that
int parsedInt;
try {
// Parsing a simple integer
parsedInt = int.Parse(valueStr1);
Console.WriteLine($"{parsedInt}");

// Parsing a floating point number, ensuring the decimal is .00 for successful integer parsing
parsedInt = int.Parse(valueStr2, NumberStyles.Float, CultureInfo.InvariantCulture);
Console.WriteLine($"{parsedInt}");

// Parsing a string with a thousand separator
parsedInt = int.Parse(valueStr3, NumberStyles.AllowThousands, CultureInfo.InvariantCulture);
Console.WriteLine($"{parsedInt}");

// Attempting to parse a string with both thousands separator and decimal
// Requires adjusting NumberStyles
parsedInt = int.Parse(valueStr4, NumberStyles.AllowThousands | NumberStyles.Float, CultureInfo.InvariantCulture);
Console.WriteLine($"{parsedInt}");

// Parsing boolean values
Console.WriteLine($"{bool.Parse("False")}");

// Parsing and formatting floating point numbers
Console.WriteLine($"{float.Parse("2.718", CultureInfo.InvariantCulture):F2}");
}
catch {
Console.WriteLine("Conversion failed");
}

// Demonstrating TryParse, which avoids exceptions by returning a boolean result
bool isParsed = false;
isParsed = int.TryParse(valueStr1, out parsedInt);
if (isParsed) {
Console.WriteLine($"{parsedInt}");
}
}

Handling Parsing Errors with Try-Catch

When converting strings to different types, things can go wrong, leading to exceptions that can crash your application. For instance, attempting to parse a string that does not properly represent a number (e.g., parsing “abc” as an integer) will throw a FormatException. To safeguard against such errors and enhance application robustness, we use try-catch blocks. This approach allows us to catch exceptions that occur during the parsing process and handle them gracefully, preventing the application from crashing. We will delve deeper into exception handling in a later in a separate post.

Introduction to Functions in C#

Functions (or methods) in C# organize code into reusable units. They can accept parameters, return values, and be invoked from other parts of a program.

using System;

// Grouping reusable code into functions allows for more modular and customizable code.

// This function converts Celsius to Fahrenheit. It has a return type, a name, and parameters.
double CelsiusToFahrenheit(double celsius) {
double result = (celsius * 9 / 5) + 32;
return result;
}

// Functions without a return value are defined with 'void'.
void DisplayMessage(string message) {
Console.WriteLine($"-->> {message}");
}

// Invoking the temperature conversion function
Console.WriteLine($"25 degrees Celsius is {CelsiusToFahrenheit(25.0)} degrees Fahrenheit.");
Console.WriteLine($"0 degrees Celsius is {CelsiusToFahrenheit(0.0)} degrees Fahrenheit.");

// Invoking the message display function
DisplayMessage("Welcome to C# functions.");
DisplayMessage("This demonstrates simple function usage.");

Named and Default Parameters in Methods

C# methods can be enhanced with named and default parameters, which improve readability and flexibility. Named parameters allow you to specify arguments in any order by naming them, which is especially useful when a method has multiple parameters. Default parameters enable you to specify default values for parameters, simplifying method calls when the default value is sufficient.

using System;

// This method allows setting a default prefix, demonstrating default parameter values.
void DisplayMessage(string message, string prefix = "")
{
Console.WriteLine($"{prefix}{message}");
}

// Testing the method with the default prefix.
DisplayMessage("Hello there!");

// Testing the method with a custom prefix.
DisplayMessage("Hello there!", ">: ");

// Invoking the method with named parameters for clarity and flexibility.
DisplayMessage(prefix: "% ", message: "Hello there!");

Passing Arguments by Value and Reference

By default, C# passes arguments to methods by value, meaning a copy of the argument is passed. Changes to the argument inside the method do not affect the original variable. However, by using the ref keyword, you can pass arguments by reference, allowing the method to modify the original variable's value.

Additionally, the out keyword enables a method to return multiple values as output parameters.

class Program {
// Passing arguments by value. Modifications inside the method won't affect the original variable.
static void IncrementByValue(int number) {
number += 5;
Console.WriteLine($"Inside IncrementByValue: {number}");
}

// Passing arguments by reference with 'ref'. Changes in the method affect the original variable.
static void IncrementByReference(ref int number) {
number += 5;
Console.WriteLine($"Inside IncrementByReference: {number}");
}

// Using 'out' to return multiple values from a method. 'out' parameters must be assigned within the method.
static void CalculateBoth(int num1, int num2, out int sum, out int difference) {
sum = num1 + num2;
difference = num1 - num2;
}

static void Main(string[] args) {
int originalValue = 15;

// Demonstrating passing by value. The original value remains unchanged outside the method.
IncrementByValue(originalValue);
Console.WriteLine($"After IncrementByValue: {originalValue}");

// Demonstrating passing by reference with 'ref'. The original value is modified.
IncrementByReference(ref originalValue);
Console.WriteLine($"After IncrementByReference: {originalValue}");

// Demonstrating using 'out' to return multiple values from a method.
int resultSum, resultDifference;
CalculateBoth(10, 5, out resultSum, out resultDifference);
Console.WriteLine($"Sum: {resultSum}, Difference: {resultDifference}");
}
}

Returning Multiple Outputs Using Tuples

C# tuples offer a convenient way to return multiple values from a method without using out parameters. Tuples are lightweight and easy to use.

class Program {
static void Main(string[] args) {
// Demonstrating the use of tuples in C# for grouping values
(int x, int y) numbersTuple = (5, 10);
var mixedTuple = ("Initial text", 10.5);

// Tuples allow for mutable operations on their elements
numbersTuple.y = 20;
mixedTuple.Item1 = "Updated text"; // if you didn't assigned a variable name it will auto assign a name as item

Console.WriteLine($"{numbersTuple.x}, {numbersTuple.y}");
Console.WriteLine($"{mixedTuple.Item1}, {mixedTuple.Item2}");

// Demonstrating how functions can interact with tuples
var operationResults = CalculateBoth(6, 12);
Console.WriteLine($"Sum: {operationResults.sum}, Product: {operationResults.product}");
}

// This function showcases how to return multiple values from a method using a tuple.
// Tuples are particularly useful for returning two or more calculated values.
static (int sum, int product) CalculateBoth(int num1, int num2) {
int sum = num1 + num2;
int product = num1 * num2;
return (sum, product); // Returns a tuple containing both the sum and product.
}
}

Object-Oriented Programming: Defining Classes

Classes are the foundation of object-oriented programming in C#. They encapsulate data and behavior into objects.

using System;

namespace Defining
{
// Defining a Car class with properties and methods
public class Car
{
// Private fields to hold car data
private string _model;
private string _manufacturer;
private int _year;

// Constructor to initialize the Car objects
public Car(string model, string manufacturer, int year) {
_model = model;
_manufacturer = manufacturer;
_year = year;
}

// Method to retrieve the car's description
public string GetDescription() {
return $"{_year} {_manufacturer} {_model}";
}
}
}
using System;

namespace Defining
{
class Program
{
static void Main(string[] args)
{
// Instantiating new Car objects with the "new" keyword
Car car1 = new Car("Model S", "Tesla", 2020);
Car car2 = new Car("Mustang", "Ford", 1968);

// Invoking a method on the Car objects to get their descriptions
Console.WriteLine(car1.GetDescription());
Console.WriteLine(car2.GetDescription());

// The following attempt to set a property directly will result in a compilation error
// because the fields are private and not accessible from outside the Car class
// car1._model = "Cybertruck"; // Uncommenting this line will cause an error
}
}
}

Access Modifiers: Controlling Visibility

Access modifiers control the accessibility of classes and members, allowing for encapsulation and protection of data

public: method or class member can be access by any other code within your program.

protected: method or class members can be accessed only be code within the class or a subclass

private: Method or class member can only be accessed by the code within the class definition itself.

using System;

namespace Defining
{
// Defining a Car class with properties and methods
class Car
{
private string _model;
private string _manufacturer;
private int _year;
private decimal _price;

public Car(string model, string manufacturer, int year) {
_model = model;
_manufacturer = manufacturer;
_year = year;
}

public string Model {
get => _model;
set => _model = value;
}

public string Manufacturer {
get => _manufacturer;
set => _manufacturer = value;
}

public int Year {
get => _year;
set => _year = value;
}

// Computed property example
public string Description {
get => $"{Model} by {Manufacturer}, Year: {Year}";
}

// Auto-implemented properties for price and VIN
public decimal Price {
get; set;
}

public string VIN {
get; set;
}
}
}
using System;

namespace Defining
{
class Program {
static void Main(string[] args) {
// Creating instances of Car and utilizing properties
Car car1 = new Car("Model S", "Tesla", 2020) {
Price = 79999.99m,
VIN = "5YJSA1E26JF123456"
};

Car car2 = new Car("Mustang", "Ford", 1968) {
Price = 35000.00m,
VIN = "1FAFP45X4WF185256"
};

// Demonstrating access to properties
Console.WriteLine(car1.Description);
Console.WriteLine($"Price: {car1.Price}, VIN: {car1.VIN}");

Console.WriteLine(car2.Description);
Console.WriteLine($"Price: {car2.Price}, VIN: {car2.VIN}");
}
}

Inheritance: Extending Classes

Inheritance allows you to create a new class that reuses, extends, or modifies the behavior of another class.

namespace Inheritance
{
// Base class defining a generic car
class Car
{
private string _model;
private string _manufacturer;
private int _year;

public Car(string model, string manufacturer, int year) {
_model = model;
_manufacturer = manufacturer;
_year = year;
}

public string Model {
get => _model;
set => _model = value;
}

public string Manufacturer {
get => _manufacturer;
set => _manufacturer = value;
}

public int Year {
get => _year;
set => _year = value;
}

// Virtual method to be overridden in derived classes
public virtual string GetDescription() {
return $"{Model} by {Manufacturer}, Year: {Year}";
}
}
}
namespace Inheritance
{
// ElectricCar is derived from the Car class
class ElectricCar : Car
{
private int _batteryCapacity;
private int _range;

// Constructor for ElectricCar includes base class initialization
public ElectricCar(string model, string manufacturer, int year, int batteryCapacity, int range)
: base(model, manufacturer, year) {
_batteryCapacity = batteryCapacity;
_range = range;
}

public int BatteryCapacity {
get => _batteryCapacity;
set => _batteryCapacity = value;
}

public int Range {
get => _range;
set => _range = value;
}

// Overriding the GetDescription method to include ElectricCar specifics
public override string GetDescription() {
return $"{base.GetDescription()}, Battery: {BatteryCapacity} kWh, Range: {Range} miles";
}
}
}
namespace Inheritance
{
class Program
{
static void Main(string[] args)
{
// Creating an instance of Car
Car myCar = new Car("Mustang", "Ford", 1968);

// Creating an instance of ElectricCar
ElectricCar myElectricCar = new ElectricCar("Model S", "Tesla", 2020, 100, 370);

// Demonstrating polymorphism with the GetDescription method
Console.WriteLine(myCar.GetDescription());
Console.WriteLine(myElectricCar.GetDescription());
}
}
}

Conclusion

We’ve covered significant ground in this post, exploring strings, StringBuilder, parsing, functions, and the basics of OOP in C#. These concepts are fundamental to becoming proficient in C# programming.

Stay tuned for our next post, where we’ll dive into commonly used data structures in C#. Feel free to leave your questions or comments below. I’m here to help!

--

--

Rashmi Ireshe

Senior Software Engineer at Sysco LABS Sri Lanka with expertise in Java development and AWS