PONY λ M2 Modula-2

PHP.CodeCompared.To/C#

An interactive executable cheatsheet comparing PHP and C#

PHP 8.3.11 (Judge0) or PHP 8.5.5 (WASM) C# 13
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 constructorsclass 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.