Output & Running
Hello, World
<?php
echo "Hello, World!\n"; Console.WriteLine("Hello, World!"); C# 9 introduced top-level statements, so a one-line program needs no class or
Main method — much like a PHP script without the <?php tag. Output goes through Console.WriteLine, which appends a newline for you, so you do not write \n.Interpreted vs Compiled
<?php
// PHP is interpreted — run the source directly:
// php script.php
$name = "World";
echo "Hello, {$name}!\n"; // C# compiles to IL, then runs on the .NET runtime:
// dotnet run (build + run)
// dotnet build (produce a DLL)
var name = "World";
Console.WriteLine($"Hello, {name}!"); C# is compiled ahead of time to intermediate language (IL) and then JIT-compiled by the .NET runtime — there is a build step PHP does not have. The payoff is a static type checker that catches whole classes of bugs before the program ever runs.
String Interpolation
<?php
$name = "Alice";
$age = 30;
echo "{$name} is {$age}\n"; var name = "Alice";
var age = 30;
Console.WriteLine($"{name} is {age}"); C#'s
$"..." interpolated strings will look instantly familiar: they embed expressions in braces just like PHP's "{$name}". The difference is the leading $ marks the whole string as interpolated, and braces hold expressions without a sigil.Variables & Types
Variables & var
<?php
$count = 42;
$price = 9.99;
$name = "Alice";
$active = true;
echo "{$count} {$price} {$name}\n"; int count = 42;
double price = 9.99;
string name = "Alice";
bool active = true;
var inferred = 42; // 'var' infers the static type (still int)
Console.WriteLine($"{count} {price} {name}"); Variables carry no
$ sigil and each has a static type, declared explicitly or inferred with var. Crucially, var is not PHP's dynamic variable — the type is fixed at compile time, so var x = 42; makes x an int forever.Nullable Types
<?php
function findAge(?string $name): ?int {
return $name === null ? null : strlen($name);
}
$result = findAge(null);
echo $result ?? -1, "\n"; // -1 int? FindAge(string? name) =>
name is null ? null : name.Length;
int? result = FindAge(null);
Console.WriteLine(result ?? -1); // -1 PHP's nullable type syntax
?int came from C#, where the same idea is written int?. C# goes further with nullable reference types (string? vs string), letting the compiler warn you about a possible null dereference — a guarantee PHP cannot make.Null Coalescing & Conditional
<?php
$config = ["timeout" => null];
echo $config["timeout"] ?? 30, "\n"; // 30
$user = null;
echo $user?->name ?? "guest", "\n"; // guest var timeout = (int?)null;
Console.WriteLine(timeout ?? 30); // 30
string? user = null;
Console.WriteLine(user?.ToUpper() ?? "GUEST"); // GUEST Both the null-coalescing operator
?? and the null-safe access operator ?-> (C#'s ?.) were adopted by PHP from C#. They behave the same: ?? supplies a fallback when the left side is null, and ?. short-circuits to null instead of throwing.Numbers & Division
<?php
echo intdiv(7, 2), "\n"; // 3
echo 7 / 2, "\n"; // 3.5
echo 7 % 2, "\n"; // 1
echo 2 ** 10, "\n"; // 1024 Console.WriteLine(7 / 2); // 3 (int division truncates)
Console.WriteLine(7 / 2.0); // 3.5 (one operand is double)
Console.WriteLine(7 % 2); // 1
Console.WriteLine(Math.Pow(2, 10));// 1024 A trap for PHP developers: in C#,
7 / 2 is integer division yielding 3, because both operands are int — unlike PHP, where / always promotes to float. Make one operand a double (7 / 2.0) to get 3.5. There is no ** operator; use Math.Pow.Strings
Common Methods
<?php
$text = " Hello, World ";
echo strtoupper(trim($text)), "\n"; // HELLO, WORLD
echo str_replace("World", "C#", "Hello World"), "\n";
echo str_contains("Hello", "ell") ? "yes\n" : "no\n"; var text = " Hello, World ";
Console.WriteLine(text.Trim().ToUpper()); // HELLO, WORLD
Console.WriteLine("Hello World".Replace("World", "C#"));
Console.WriteLine("Hello".Contains("ell") ? "yes" : "no"); Strings are objects with methods, so you chain
text.Trim().ToUpper() rather than nesting strtoupper(trim($text)). The method names are PascalCase (Replace, Contains), the C# convention for all public members.Formatting & Padding
<?php
$price = 1234.5;
echo "$" . number_format($price, 2), "\n"; // $1,234.50
echo str_pad("7", 3, "0", STR_PAD_LEFT), "\n"; // 007 var price = 1234.5;
Console.WriteLine($"${price:N2}"); // $1,234.50
Console.WriteLine($"{7:D3}"); // 007 C# folds formatting into interpolated strings with a format specifier after a colon:
{price:N2} means a number with thousands separators and two decimals. It covers the same ground as number_format and str_pad but inline at the interpolation site.Split & Join
<?php
$csv = "a,b,c";
$parts = explode(",", $csv);
print_r($parts);
echo implode("-", $parts), "\n"; // a-b-c var csv = "a,b,c";
string[] parts = csv.Split(',');
Console.WriteLine(string.Join(", ", parts)); // a, b, c
Console.WriteLine(string.Join("-", parts)); // a-b-c The pair is
Split / string.Join rather than explode / implode. Split is a string method returning a string[], while Join is a static method on string taking the separator first — the reverse argument order from implode.Arrays vs Collections
One Array vs Typed Collections
<?php
// PHP's single array is both list and map:
$list = [1, 2, 3];
$map = ["name" => "Alice", "age" => 30];
echo $list[0], " ", $map["name"], "\n"; // C# has distinct, statically typed collections:
List<int> numbers = [1, 2, 3];
Dictionary<string, string> person = new()
{
["name"] = "Alice",
["age"] = "30",
};
var name = person["name"];
Console.WriteLine($"{numbers[0]} {name}"); PHP's one ordered-map array splits into separate generic types in C#:
List<T> for ordered sequences and Dictionary<TKey, TValue> for key-value maps, each with its element types fixed at compile time. The [1, 2, 3] collection-expression syntax is C# 12+.List Operations
<?php
$numbers = [3, 1, 2];
$numbers[] = 4; // append
sort($numbers);
echo count($numbers), "\n"; // 4
echo in_array(2, $numbers) ? "yes\n" : "no\n"; List<int> numbers = [3, 1, 2];
numbers.Add(4);
numbers.Sort();
Console.WriteLine(numbers.Count); // 4
Console.WriteLine(numbers.Contains(2)); // True A
List<T> exposes methods rather than free functions: numbers.Add(4), numbers.Sort(), numbers.Count, and numbers.Contains(2) replace array_push, sort, count, and in_array. Note Count is a property, not a method call.Dictionary Operations
<?php
$person = ["name" => "Alice"];
$person["age"] = "30";
echo isset($person["name"]) ? "yes\n" : "no\n";
echo $person["email"] ?? "none", "\n"; // none
foreach ($person as $key => $value) {
echo "{$key}={$value} ";
}
echo "\n"; var person = new Dictionary<string, string> { ["name"] = "Alice" };
person["age"] = "30";
Console.WriteLine(person.ContainsKey("name")); // True
Console.WriteLine(person.GetValueOrDefault("email", "none")); // none
foreach (var (key, value) in person)
{
Console.Write($"{key}={value} ");
}
Console.WriteLine(); Key existence is
ContainsKey (the parallel of isset), and the safe lookup with a default is GetValueOrDefault — the counterpart to PHP's ?? on an array index. Iterating with foreach (var (key, value) in person) deconstructs each entry, much like foreach ... as $key => $value.Tuples & Deconstruction
<?php
// PHP destructures arrays with [] or list():
[$x, $y] = [10, 20];
echo "{$x},{$y}\n"; // 10,20
[$x, $y] = [$y, $x]; // swap
echo "{$x},{$y}\n"; // 20,10 (int x, int y) point = (10, 20);
var (a, b) = point; // deconstruction
Console.WriteLine($"{a},{b}"); // 10,20
(a, b) = (b, a); // swap, no temp
Console.WriteLine($"{a},{b}"); // 20,10 C# has real tuples with named or positional elements:
(int x, int y). Deconstructing with var (a, b) = point generalises PHP's array destructuring and makes the swap idiom (a, b) = (b, a) read cleanly — a typed, first-class feature rather than an array trick.Control Flow
Conditionals
<?php
$score = 85;
if ($score >= 90) {
echo "A\n";
} elseif ($score >= 80) {
echo "B\n";
} else {
echo "C\n";
} var score = 85;
if (score >= 90)
{
Console.WriteLine("A");
}
else if (score >= 80)
{
Console.WriteLine("B");
}
else
{
Console.WriteLine("C");
} The structure is nearly identical, with two cosmetic differences: the keyword is
else if (two words) rather than elseif, and the C# convention places each brace on its own line. Conditions still need parentheses, exactly as in PHP.Loops & foreach
<?php
for ($index = 0; $index < 3; $index++) {
echo $index, " ";
}
echo "\n";
foreach (["a", "b", "c"] as $letter) {
echo $letter, " ";
}
echo "\n"; for (int index = 0; index < 3; index++)
{
Console.Write($"{index} ");
}
Console.WriteLine();
foreach (var letter in new[] { "a", "b", "c" })
{
Console.Write($"{letter} ");
}
Console.WriteLine(); The classic three-part
for loop is the same in both languages. The foreach uses in instead of PHP's as, and the loop variable is typed (or var-inferred) rather than carrying a $.Named Arguments
<?php
function connect(string $host, int $port = 5432, bool $ssl = false): string {
return "{$host}:{$port} ssl=" . ($ssl ? "1" : "0");
}
echo connect("db.example.com", ssl: true), "\n"; string Connect(string host, int port = 5432, bool ssl = false) =>
$"{host}:{port} ssl={(ssl ? 1 : 0)}";
Console.WriteLine(Connect("db.example.com", ssl: true)); PHP 8's named arguments are a direct adoption of a C# feature, with identical syntax:
connect(host, ssl: true). You can skip optional parameters by naming the one you want, and named arguments must follow any positional ones.Methods & Functions
Functions & Local Functions
<?php
function add(int $first, int $second): int {
return $first + $second;
}
echo add(2, 3), "\n"; // 5 int Add(int first, int second)
{
return first + second;
}
Console.WriteLine(Add(2, 3)); // 5
// expression-bodied form, like PHP's fn():
int Multiply(int first, int second) => first * second;
Console.WriteLine(Multiply(2, 3)); // 6 A function declared at top level is a local function, with the return type written before the name (no
function keyword). The expression-bodied form => first * second mirrors PHP's arrow-function shorthand for one-liners.Variadic & Spread
<?php
function total(int ...$numbers): int {
return array_sum($numbers);
}
echo total(1, 2, 3), "\n"; // 6
$values = [4, 5, 6];
echo total(...$values), "\n"; // 15 int Total(params int[] numbers) => numbers.Sum();
Console.WriteLine(Total(1, 2, 3)); // 6
int[] values = [4, 5, 6];
Console.WriteLine(Total(values)); // 15 — pass the array directly C# marks a variadic parameter with
params rather than ..., collecting the trailing arguments into an array. To pass an existing array you hand it over directly; there is no spread operator at the call site the way PHP uses ...$values.Lambdas & Delegates
<?php
$multiplier = 3;
$scale = fn($value) => $value * $multiplier; // auto-captures
echo $scale(5), "\n"; // 15 var multiplier = 3;
Func<int, int> scale = value => value * multiplier; // captures automatically
Console.WriteLine(scale(5)); // 15 A C# lambda
value => value * multiplier is the counterpart to PHP's arrow function and captures surrounding variables automatically. Its type is a delegate such as Func<int, int> (arguments then return type) or Action<T> for a void result.First-Class Method References
<?php
$names = ["alice", "bob", "carol"];
// PHP 8.1 first-class callable syntax:
$lengths = array_map(strlen(...), $names);
print_r($lengths); // 5, 3, 5 string[] names = ["alice", "bob", "carol"];
// method group — pass the method by name, no lambda:
var lengths = names.Select(name => name.Length).ToArray();
Console.WriteLine(string.Join(", ", lengths)); // 5, 3, 5 PHP 8.1's first-class callable syntax
strlen(...) echoes C#'s long-standing method groups, where a method name alone can stand in for a delegate. In C# you commonly write a short lambda instead, as here, because there is no free length function to reference.Classes & OOP
Defining a Class
<?php
class Greeter {
public function __construct(private string $name) {}
public function greet(): string {
return "Hello, {$this->name}!";
}
}
$greeter = new Greeter("Alice");
echo $greeter->greet(), "\n"; var greeter = new Greeter("Alice");
Console.WriteLine(greeter.Greet());
class Greeter(string name) // primary constructor (C# 12+)
{
public string Greet() => $"Hello, {name}!";
} C# 12 added primary constructors —
class Greeter(string name) — strikingly close to PHP 8's constructor property promotion. Member access uses a dot (greeter.Greet()) instead of ->, and there is no $this: you reference members directly, or via this. when disambiguating.Inheritance
<?php
abstract class Animal {
public function __construct(protected string $name) {}
abstract public function speak(): string;
}
class Dog extends Animal {
public function speak(): string {
return "{$this->name} says Woof";
}
}
echo (new Dog("Rex"))->speak(), "\n"; Console.WriteLine(new Dog("Rex").Speak());
abstract class Animal(string name)
{
protected string Name => name;
public abstract string Speak();
}
class Dog(string name) : Animal(name)
{
public override string Speak() => $"{Name} says Woof";
} A subclass uses a colon (
class Dog : Animal) where PHP uses extends, and overriding a virtual or abstract member requires the explicit override keyword — the compiler rejects an accidental override, which PHP allows silently. Call the base with base.Method(), PHP's parent::.Interfaces
<?php
interface Shape {
public function area(): float;
}
class Circle implements Shape {
public function __construct(private float $radius) {}
public function area(): float {
return 3.14159 * $this->radius ** 2;
}
}
echo (new Circle(2))->area(), "\n"; Console.WriteLine(new Circle(2).Area());
interface IShape
{
double Area();
}
class Circle(double radius) : IShape
{
public double Area() => 3.14159 * radius * radius;
} Interfaces work as in PHP, with two conventions to note: C# interface names are prefixed with
I (IShape), and a class lists both base class and interfaces after a single colon, separated by commas. There is no implements keyword.Static Members
<?php
class MathUtil {
public const float PI = 3.14159;
public static function square(float $value): float {
return $value * $value;
}
}
echo MathUtil::square(5), "\n"; // 25
echo MathUtil::PI, "\n"; Console.WriteLine(MathUtil.Square(5)); // 25
Console.WriteLine(MathUtil.Pi);
static class MathUtil
{
public const double Pi = 3.14159;
public static double Square(double value) => value * value;
} Static members are accessed with a plain dot (
MathUtil.Square(5)) rather than the :: scope-resolution operator. A class marked static can only hold static members — a clean way to express a utility class that should never be instantiated.Properties & Records
Properties vs Magic Methods
<?php
class Temperature {
public function __construct(private float $celsius) {}
public function __get($name) {
if ($name === "fahrenheit") {
return $this->celsius * 9 / 5 + 32;
}
}
}
$temp = new Temperature(20);
echo $temp->fahrenheit, "\n"; // 68 var temperature = new Temperature(20);
Console.WriteLine(temperature.Fahrenheit); // 68
class Temperature(double celsius)
{
public double Fahrenheit => celsius * 9 / 5 + 32; // computed property
} C# has real properties:
Fahrenheit looks like a field at the call site but runs a getter. This replaces PHP's __get / __set magic methods with first-class, type-checked, individually named members — no string dispatch and no surprises.Auto-Properties & init
<?php
class Point {
public function __construct(
public readonly int $x,
public readonly int $y,
) {}
}
$point = new Point(3, 4);
echo "{$point->x},{$point->y}\n"; // 3,4 var point = new Point { X = 3, Y = 4 };
Console.WriteLine($"{point.X},{point.Y}"); // 3,4
class Point
{
public int X { get; init; } // settable only during initialization
public int Y { get; init; }
} PHP 8.1's
readonly properties parallel C#'s init-only auto-properties: both allow a value to be set once at construction and never again. The { X = 3, Y = 4 } object initializer is a C# convenience PHP has no direct equivalent for.Records
<?php
// PHP has no record type; you write an immutable class:
final class Money {
public function __construct(
public readonly int $cents,
public readonly string $currency,
) {}
}
$a = new Money(100, "USD");
$b = new Money(100, "USD");
echo $a == $b ? "equal\n" : "different\n"; // equal (value compare) var first = new Money(100, "USD");
var second = new Money(100, "USD");
Console.WriteLine(first == second ? "equal" : "different"); // equal
Console.WriteLine(first); // Money { Cents = 100, Currency = USD }
record Money(int Cents, string Currency); A C# record generates a constructor, value-based equality, a readable
ToString, and a with expression for copies — all from one line. It is far less boilerplate than PHP's immutable class, and crucially == compares by value, whereas for a PHP object == compares property values but === checks identity.Attributes
<?php
#[Attribute]
class Route {
public function __construct(public string $path) {}
}
class Controller {
#[Route("/home")]
public function home(): string { return "home"; }
}
$method = new ReflectionMethod(Controller::class, "home");
$attribute = $method->getAttributes(Route::class)[0]->newInstance();
echo $attribute->path, "\n"; // /home using System.Reflection;
var method = typeof(Controller).GetMethod("Home")!;
var route = method.GetCustomAttribute<RouteAttribute>()!;
Console.WriteLine(route.Path); // /home
[AttributeUsage(AttributeTargets.Method)]
class RouteAttribute(string path) : Attribute
{
public string Path => path;
}
class Controller
{
[Route("/home")]
public string Home() => "home";
} PHP 8's attributes — and even the
#[...] syntax — were modelled on C#'s, which use [...]. The mechanics match closely: declare an attribute class deriving from Attribute, annotate a member, then read it back via reflection. C# resolves the Route suffix to RouteAttribute automatically.Pattern Matching & Enums
match vs switch Expression
<?php
$status = 404;
$message = match (true) {
$status < 300 => "ok",
$status < 400 => "redirect",
$status < 500 => "client error",
default => "server error",
};
echo $message, "\n"; // client error var status = 404;
var message = status switch
{
< 300 => "ok",
< 400 => "redirect",
< 500 => "client error",
_ => "server error",
};
Console.WriteLine(message); // client error PHP 8's
match expression was inspired by C#'s switch expression. Both return a value and require exhaustiveness, but C# arms are patterns — here relational patterns like < 300 — and the discard _ is the default, where PHP uses the default keyword.Type & Property Patterns
<?php
// PHP matches types with instanceof and manual checks:
function describe(mixed $value): string {
if (is_int($value) && $value > 0) {
return "positive int";
}
if (is_string($value)) {
return "string of length " . strlen($value);
}
return "other";
}
echo describe(42), "\n"; // positive int
echo describe("hi"), "\n"; // string of length 2 string Describe(object value) => value switch
{
int n when n > 0 => "positive int",
string s => $"string of length {s.Length}",
_ => "other",
};
Console.WriteLine(Describe(42)); // positive int
Console.WriteLine(Describe("hi")); // string of length 2 C#'s pattern matching outclasses PHP's
match here: a type pattern int n both tests the type and binds a typed variable, a when clause adds a guard, and string s gives you s.Length directly. PHP falls back to is_int / instanceof with manual casts.Enums
<?php
enum Suit: string {
case Hearts = "hearts";
case Spades = "spades";
public function color(): string {
return match ($this) {
Suit::Hearts => "red",
Suit::Spades => "black",
};
}
}
echo Suit::Hearts->value, " ", Suit::Hearts->color(), "\n"; Console.WriteLine($"{Suit.Hearts} {Color(Suit.Hearts)}");
string Color(Suit suit) => suit switch
{
Suit.Hearts => "red",
Suit.Spades => "black",
_ => "unknown",
};
enum Suit { Hearts, Spades } This is one place PHP went further than classic C#: PHP enums are objects that can carry backing values and methods, while a C#
enum is fundamentally a named integer constant with no methods. The idiomatic C# approach is an extension method or a switch, as shown; string-backed values typically need attributes.LINQ vs Array Functions
Map & Filter
<?php
$numbers = [1, 2, 3, 4, 5];
$result = array_map(
fn($n) => $n * 2,
array_filter($numbers, fn($n) => $n % 2 === 1)
);
print_r(array_values($result)); // 2, 6, 10 int[] numbers = [1, 2, 3, 4, 5];
var result = numbers
.Where(n => n % 2 == 1)
.Select(n => n * 2)
.ToList();
Console.WriteLine(string.Join(", ", result)); // 2, 6, 10 LINQ's
Where and Select are the equivalents of array_filter and array_map, but they chain left to right in execution order — far more readable than PHP's inside-out nesting. LINQ is also lazy: nothing runs until you call a terminal operator like ToList().Aggregates & Reduce
<?php
$numbers = [1, 2, 3, 4, 5];
echo array_sum($numbers), "\n"; // 15
echo max($numbers), "\n"; // 5
echo array_reduce($numbers, fn($carry, $n) => $carry * $n, 1), "\n"; // 120 int[] numbers = [1, 2, 3, 4, 5];
Console.WriteLine(numbers.Sum()); // 15
Console.WriteLine(numbers.Max()); // 5
Console.WriteLine(numbers.Aggregate(1, (product, n) => product * n)); // 120 LINQ ships a rich set of aggregate operators —
Sum, Max, Min, Average, Count — as methods on any sequence, replacing PHP's free functions. The general fold is Aggregate, the counterpart to array_reduce, taking the seed first and then the combiner.Grouping & Sorting
<?php
$people = [
["name" => "Alice", "city" => "NYC"],
["name" => "Bob", "city" => "LA"],
["name" => "Carol", "city" => "NYC"],
];
$byCity = [];
foreach ($people as $person) {
$byCity[$person["city"]][] = $person["name"];
}
foreach ($byCity as $city => $names) {
echo "{$city}: " . implode(", ", $names) . "\n";
} var people = new[]
{
new { Name = "Alice", City = "NYC" },
new { Name = "Bob", City = "LA" },
new { Name = "Carol", City = "NYC" },
};
var byCity = people
.GroupBy(person => person.City)
.OrderBy(group => group.Key);
foreach (var group in byCity)
{
var names = string.Join(", ", group.Select(person => person.Name));
Console.WriteLine($"{group.Key}: {names}");
} LINQ's
GroupBy turns the manual "build an array of arrays" loop into one declarative call, and OrderBy sorts by a key selector. The new { Name = ... } syntax creates an anonymous type — a quick typed record with no declaration, handy for intermediate shapes.Error Handling
Try / Catch / Finally
<?php
try {
$result = intdiv(10, 0);
} catch (DivisionByZeroError $error) {
echo "Caught: ", $error->getMessage(), "\n";
} finally {
echo "Done\n";
} var divisor = 0;
try
{
var result = 10 / divisor;
}
catch (DivideByZeroException error)
{
Console.WriteLine($"Caught: {error.Message}");
}
finally
{
Console.WriteLine("Done");
} The structure is identical to PHP's, down to the
finally block. The exception's text is the Message property rather than a getMessage() call, and C# ships a deep hierarchy of built-in exception types you catch by class, just as PHP does.Throwing Exceptions
<?php
function withdraw(int $balance, int $amount): int {
if ($amount > $balance) {
throw new InvalidArgumentException("Insufficient funds");
}
return $balance - $amount;
}
try {
withdraw(100, 200);
} catch (InvalidArgumentException $error) {
echo $error->getMessage(), "\n";
} int Withdraw(int balance, int amount)
{
if (amount > balance)
{
throw new ArgumentException("Insufficient funds");
}
return balance - amount;
}
try
{
Withdraw(100, 200);
}
catch (ArgumentException error)
{
Console.WriteLine(error.Message);
} You
throw new an exception exactly as in PHP. C# has no checked exceptions (unlike Java), so a method never declares what it might throw — the same implicit model PHP uses. Reach for built-in types like ArgumentException before writing your own.using vs finally Cleanup
<?php
// PHP relies on finally for deterministic cleanup:
$handle = fopen("php://temp", "w+");
try {
fwrite($handle, "data");
rewind($handle);
echo stream_get_contents($handle), "\n";
} finally {
fclose($handle);
} // 'using' disposes the resource at end of scope — no finally:
using (var writer = new StringWriter())
{
writer.Write("data");
Console.WriteLine(writer.ToString());
} // writer.Dispose() called automatically here C#'s
using statement guarantees that any IDisposable resource is released when the block ends — even on an exception — replacing the manual try ... finally fclose() dance. The using var writer = ...; declaration form ties the lifetime to the enclosing scope with no extra braces.Async & Await
Async & Await
<?php
// PHP executes synchronously by default; concurrency needs
// extensions (Fibers, Swoole, ReactPHP). A plain script blocks:
function fetchValue(): int {
// imagine a slow I/O call here
return 42;
}
echo fetchValue(), "\n"; // 42 async Task<int> FetchValueAsync()
{
await Task.Delay(10); // non-blocking await
return 42;
}
Console.WriteLine(await FetchValueAsync()); // 42 Async is built into C#: an
async method returns a Task<T> and you await it without blocking a thread. PHP has no language-level async — concurrency requires Fibers or an extension like Swoole — so this is a genuine capability gap rather than a syntax difference. Top-level await works in a C# program file.Awaiting Many Tasks
<?php
// To run several operations "in parallel", classic PHP just
// runs them one after another — there is no built-in Task.WhenAll:
function square(int $n): int { return $n * $n; }
$results = array_map('square', [1, 2, 3]);
print_r($results); // 1, 4, 9 async Task<int> SquareAsync(int n)
{
await Task.Delay(10);
return n * n;
}
int[] results = await Task.WhenAll(
SquareAsync(1), SquareAsync(2), SquareAsync(3));
Console.WriteLine(string.Join(", ", results)); // 1, 4, 9 Task.WhenAll launches multiple async operations concurrently and awaits them all, returning an array of results — the kind of structured concurrency PHP can only achieve with an event-loop library. For CPU-bound parallelism C# also offers Parallel.ForEach and PLINQ.⚠ Gotchas for PHP Devs
Value vs Reference Types
<?php
// PHP arrays are value types — assignment COPIES:
$original = [1, 2, 3];
$copy = $original;
$copy[] = 4;
echo count($original), "\n"; // 3 — unchanged // A List is a reference type — assignment ALIASES:
List<int> original = [1, 2, 3];
var alias = original;
alias.Add(4);
Console.WriteLine(original.Count); // 4 — changed!
var realCopy = new List<int>(original); // explicit copy
Console.WriteLine(realCopy.Count); // 4
realCopy.Add(5);
Console.WriteLine(original.Count); // still 4 A crucial reversal: PHP arrays are value types, so
$copy = $original duplicates them, whereas a C# List<T> is a reference type, so alias = original makes two names for one object. Structs and records-with-value-equality behave differently again — know whether a type is a class or a struct.Equality & Type Juggling
<?php
// PHP: == is loose, === is strict (value + type):
var_dump("1" == 1); // true (loose!)
var_dump("1" === 1); // false (strict)
var_dump(0 == "abc"); // false in PHP 8 (was true pre-8) // C# == never juggles types; mismatched types won't compile:
Console.WriteLine(1 == 1); // True
Console.WriteLine("1" == "1"); // True (string value equality)
Console.WriteLine(Equals("1", 1)); // False — different types
// Console.WriteLine("1" == 1); // compile error! C# has no loose
== and no type juggling: comparing a string to an int does not even compile. You never need PHP's === distinction. For reference types == checks identity by default, while record types and overloaded operators (like string) compare by value.No Undefined — Default Values
<?php
// PHP: reading a missing array key warns and yields null:
$data = [];
$value = $data["missing"] ?? "fallback";
echo $value, "\n"; // fallback
// An uninitialized typed property throws on access. // C#: value types have a zero default, references default to null:
int number = default; // 0
bool flag = default; // false
string? text = default; // null
var shownText = text ?? "null";
Console.WriteLine($"{number} {flag} {shownText}"); // 0 False null
// Reading a missing Dictionary key THROWS — use TryGetValue. There is no
undefined in C#. Every type has a default — zero for numbers, false for bool, null for reference types. Beware: indexing a Dictionary with an absent key throws a KeyNotFoundException rather than returning null, so use TryGetValue or GetValueOrDefault.Naming & Casing
<?php
class userAccount {
public string $userName = "alice";
public function getDisplayName(): string {
return $this->userName;
}
}
$account = new userAccount();
echo $account->getDisplayName(), "\n"; // alice var account = new UserAccount();
Console.WriteLine(account.GetDisplayName()); // alice
class UserAccount
{
public string UserName { get; set; } = "alice";
public string GetDisplayName() => UserName;
} C# conventions are strict and pervasive: PascalCase for classes, methods, and public properties (
UserAccount, GetDisplayName, UserName), and camelCase for locals and parameters. This differs from common PHP habits like $userName properties and camelCase methods, so adjust your muscle memory.