Java vs C# - Part 3

Introduction

Updated records, thanks, Tim Purdum, for your review!

This is my third post on the syntax of Java versus that of C#. It's been very long since I wrote the first two, and a lot has changed in C# (and something in Java); I updated the original ones, which you can find here (will be moving to this blog shortly), and this one will cover what's new. It's going to be a bit longer, as there are lots of features to cover, especially coming from C#. Again, this is not about each frameworks' specific APIs or features, it's just about how the C# and the Java languages differ. Because I'm a .NET person, I will give examples in C# only.

I'm going to talk about:

  • Init-only properties
  • Primary constructors
  • Tuples
  • Interface members and default interface methods
  • Partial properties
  • Async/await
  • Expression body members
  • Pattern matching
  • Local functions
  • Records
  • Global imports
  • Type aliases
  • Implicitly-typed variables
  • Top-level statements
  • Conditional member access
  • Null-coalescing operators
  • Calling external methods
  • nameof operator
  • with expression
  • Anonymous types
  • Unsafe operations

Init-only Properties

In .NET/C# we have properties as a first-class construct, not just as methods (getXXX/setXXX as in Java). We can have properties:

  • With a backing field (and/or some logic)
  • Auto-declared, with a default backing field
  • With different visibilities for setter and getter
  • With just a getter, meaning, they can only be assigned from a constructor (read-only properties)
  • With just a setter
  • Marked as init-only, meaning, they can be set from a construction expression, but never again (similar to having just a getter)
  • With default values
  • With an expression body (more below)

Read more here: https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/properties.

public record ImmutablePoint
{
    public int X { get; init; } = 0; //init-only with default value
    public int Y { get; init; } = 0;
    public double Dist => Math.Sqrt(X * X + Y * Y); //expression body
    private int _altitude;
    public int Altitude { get => _altitude; set => _altitude = value; } //backing field
    public string Name { get; private set; }
    public void SetName()
    {
        if (X == 0 && Y == 0)
        {
            Name = "Center of the World";
        }
        else
        {
            Name = "Some random location";
        }
    }
}
var point = new ImmutablePoint { X = 1, Y = -10 };

Primary Constructors

In C#, to make things easier, it's possible to declare fields as part of a "primary constructor", where they are declared together with the type. There can be only one primary constructor, but other constructors can be defined. A primary constructor is always public and the parameters it declares are treated as private fields.

Read more here: https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/tutorials/primary-constructors.

public class Person(string name, DateTime birthday, string email) //constructor together with the type declaration
{
    public string Name =>  name; //if we want, we can expose the fields as properties
    public DateTime Birthday => birthday;
    public string Email => email;
}

Tuples

A tuple is a construct that consists of an anonymous type that has one or more fields. This can be used as the return type of a method, so that we can return multiple values at once. C# supports this out of the box, behind the scenes, the type inherits from one of the Tuple<> types.

See the official definition here: https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/value-tuples.

An example:

public (int x, int y) GetPoint() => (x: 1, y: 20);
var point = GetPoint(); //using an implicitly-typed variable
(int x, int y) = point; //alternative

Interface Members and Default Interface Methods

In C#, interfaces can declare methods and properties. In Java, they can also declare public static fields as well (there are no properties in Java).

In recent versions of C#, as in Java, it is now possible to declare methods with bodies on interfaces. Implementing classes can either override them or go with the default implementation on the interface.

See it here: https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-8.0/default-interface-methods and https://docs.oracle.com/javase/tutorial/java/IandI/defaultmethods.html.

An example in C#:

public interface IMyInterface
{
    string GetStringRepresentation() { return "Default"; }
}

Partial Properties

Besides partial classes and methods, C# also allows declaring partial properties. This is mostly useful when code is automatically generated but we want to give developers a way to change its functionality or complete it on another file, that is manually controlled.

Read about it here: https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/partial-classes-and-methods.

//File1.cs
public partial string Prop { get; set; }
//File1.implementation.cs
private string _prop;
public partial string Prop { get => _prop; set => _prop = value; }

Async/Await

Asynchronous programming has become prevalent in .NET and pretty much everywhere related to network programming. .NET introduced some language constructs to make developers' lifes easier when it comes to implementing asynchronous APIs. Most notorious are the async and await keywords. In a nutshell, they build transparently a state machine that runs an asynchronous method in a background task and waits for its completion, without blocking the main thread. This makes it very simple to use and has in fact been copied by other languages such as TypeScript.

Have a look at the specification here: https://learn.microsoft.com/en-us/dotnet/csharp/asynchronous-programming.

A simple example:

public async Task<int> AsyncOperation() { /*something asynchronous*/ }
var result = await AsyncOperation(); //waits for the completion of the asynchronous operation and gets its result

Expression Body Members

It is possible in C# to skip a formal method declaration and just have the definition of a method or property declared inline. This can include throw expressions (see below).


public int Add(int a, int b) => a + b;
public virtual void Process() => throw new NotImplementedException();
private int _field;
public int Property => _field;

Pattern Matching

Pattern matching is essentially switch statements on steroids. In the old C# days, we could only compare with some constant integral, string, or enumeration values, but now things can be much more complex. Java does not offer any similar mechanism. C# pattern matching includes:

  • Exact matching for discrete values
  • Null/not null check
  • Type check
  • Relational checks (>, <, >=, <=)
  • Conjunction (and, or)
  • Property matches

Reference documentation for pattern matching: https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/patterns and https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/functional/pattern-matching.

Some examples of the new syntax:

var monthName = DateTime.Today.Month switch
{
    3 or 4 or 5 => "spring",
    6 or 7 or 8 => "summer",
    9 or 10 or 11 => "autumn",
    12 or 1 or 2 => "winter",
    _ => "unknown"
};
string WaterState(int tempInFahrenheit) => tempInFahrenheit switch
{
    (> 32) and (< 212) => "liquid",
    < 32 => "solid",
    > 212 => "gas",
    32 => "solid/liquid transition",
    212 => "liquid / gas transition"
};
var location = coords switch
{
    { X: 0, Y: 0} => "center of the world",
    { X: > 0 } => "right of Greenwich",
    { Y: > 0 } => "northern hemisphere",
    _ => "somewhere on the planet"
};

Local Functions

In C#, functions (not methods!) can be declared inside a method, constructor, or property body. They are similar to lambdas, but unlike lambdas, they can have attributes applied to them or their parameters. Their scope is only the containing block. Java only has lambdas, not actual functions.

Read about local functions here: https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/local-functions.

Example:

public void DoCalculation()
{
    int Add(int a, int b) => a + b;
    Add(1, 2);
}
Add(1, 2); //error as Add is not in scope

Records

A record is a new construct in C# and Java that is essentially a class (or a struct) but has some things implemented for us:

  • Object equality
  • Hash code implementation
  • Cloning
  • A string representation that shows all properties' values

Records can use the primary constructor syntax or the same syntax that we're used to in classes and structs in C#. They are, for all effects, either a class or a struct, but the record declaration just adds a few things for us transparently. In Java, unlike C#, a record does not implement cloning, and, of course, structs only exist in .NET.

Read all about records from the source: https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/types/records and https://docs.oracle.com/en/java/javase/17/language/records.html.

A couple examples (C#):
public record Person(string Name, DateOnly Birthday, string Email);
public readonly record struct Point(double X, double Y, double Z);
public record Person
{
    public string Name { get; set; }
    public DateOnly Birthday { get; set; }
    public string Email { get; set; }
    public bool IsBirthdayToday => DateOnly.FromDateTime(DateTime.Today) == Birthday;
}

Global Imports

In both C# and Java, if we don't wish to have full namespace/package references to a type, we can import its namespace/package, either through the using (C#) or the import (Java) declarations, and just reference the type by its name. In Java, the java.lang package is automatically imported, which does not happen in C#.

In C#, we can have global imports, which save us from entering the same using statements over and over again. Global impots can be declared in code or on the .csproj file.

An example in code:

global using System.Text;

And inside the .csproj file:

<ItemGroup>
    <Using Include="System.Text"/>
</ItemGroup>

Type Aliases

In C#, we can give an existing type a new name, for example, string is the same as System.String, int is System.Int32, bool is System.Boolean, etc. This was actually referred in part 1 of this series, I'll just add an example on global aliases.

using date = System.DateOnly; //date can be used instead of System.DateOnly on the file
global using time = System.TimeOnly; //time can be used instead of System.TimeOnly everywhere
global using static System.Math; //Math can be used instead of System.Math everywhere

Implicitly-typed Variables

In C# and Java it is possible to declare a variable at the same time a value is assigned to it, without the need of the type declaration, as it is inferred from the assignement. It is, of course, illegal to have an implicitly-typed variable without an assignment, or with an assignment that is not clear as to its type, such as null.

Read about implicitly-typed variables here: https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/implicitly-typed-local-variables and https://docs.oracle.com/en/java/javase/13/language/local-variable-type-inference.html.

var x = 10;
var y = SomeMethod();
var z = null; //error

Top Level Statements

In C#, it is now possible to skip any top-level methods, including Main, and just go with declarations. They will be treated as if part of an unnamed asynchronous method.

Console.WriteLine("Hello, World, from a Main-less program!");

Conditional Member Access

In C#, we don't need to keep checking if a (possibly nested) member is null or not, we just use the ?. dotted operator and if any of the members is null, it will just return null at the end. If the last member access is not nullable (value or reference), the result will be implicitly of that same type, but nullable.

See more here: https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/member-access-operators#null-conditional-operators--and-.

var valueOrNull = someObject?.SomeProperty?.SomeNestedProperty;

Null-coalescing and Null-conditional Operators

In C#, we can apply an operator to an expression so that when the expression returns null, an alternative value is provided. We can also assign it a value only if it's currently null. This skips writing if conditions, and is just a simplified form.

Refereces are: https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/null-coalescing-operator and https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/member-access-operators#null-conditional-operators--and-.

var a = b ?? c;  //a will be c only if b is null
a ??= "not null if was null"; //if a is null, it will be assigned this string

Throw Operator

Similar to the null-coalescing operator, we can check if an expression returns null, and, if so, throw an exception. It's the throw expression.

var a = b ?? throw new Exception("b is null"); //if b is null an exception is thrown

Calling External Methods

In both Java and C# is it possible to state that a method is implemented externally, from an imported native library. This is achieved through the native (Java) and extern (C#) keywords and is called Java Native Interface (JNI) and in .NET, Native Interoperability, and has been a feature of both frameworks since the start.

References: https://docs.oracle.com/javase/1.5.0/docs/guide/jni/spec/intro.html and https://learn.microsoft.com/en-us/dotnet/standard/native-interop.

In .NET, we need to tell the compiler where to find the method, using the [DllImport] attribute, it's what we use to tell .NET where to look for the external function as in the next example (user32.dll).

[DllImport("user32.dll")]
public static extern bool MessageBeep(int uType);

nameof Operator

In C#, the nameof operator returns the name of a type, method, property, or field. It's just a useful way to have a string constant that is refactor-friendly, meaning, if you change its target, it will change as well.

Documentation here: https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/nameof.

Examples:

var className = nameof(MyClass); //"MyClass"
var propName = nameof(MyClass.MyProp); //"MyProp"
var methodName = nameof(MyClass.MyMethod); //"MyMethod"

with Expression

with expressions are a way to build an object from an existing one, with some modifications, which are expressed as init expressions. Modifications, of course, must be mutable or init-only properties.

The reference is available at https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/with-expression.

An example:
record struct Point(int X, int Y);
var original = new Point(100, 50);
var moved = original with { X = 200 };

Anonymous Types

In Java, we can create anonymous types that implement an interface or extend some class; we must provide the implementation on the usage.

Read more here: https://docs.oracle.com/javase/tutorial/java/javaOO/anonymousclasses.html.

Example (Java):

interface Greet
{
    void sayHello();
}
new Greet()
{
    public void sayHello()
    {
        System.out.println("Hello, World!");
    }
}.sayHello();

Unsafe Operations

C# lets us perform "unsafe" operations, meaning, pointer and memory-based operations such as the ones we can do in C and C++. These include:

  • Specifying the memory layout for a class or struct
  • Memory allocation on the stack
  • Pointer operations

This is something that just doesn't exist in Java, and should be used with caution - we're going from a managed language to low-level code like the one in C/C++, so we better know what we're doing! We probably only need it for high-performance code.

Some references include:

A few examples:

int[] a = [10, 20, 30, 40, 50];
unsafe
{
    int* original = stackalloc int[a.Length]; //allocate on the stack
    fixed (int* p = &a[0])
    {
        int* p1 = p; //p is fixed, so we need another pointer
        for (var i = 0; i < a.Length; i++)
        {
            original[i] = *p1; //store the current value pointed by the pointer
            *p1 /= 10; //change the value pointed to by the pointer
            p1 += 1; //increment the pointer so that it points to the next element
        }
    }
};

Conclusion

As you can see, although both Java and C# started very similar, they have evolved a lot from the original C/C++ root, and the code can look quite strange now. I believe it's a good thing that the languages keep evolving, and, of course, we don't need to use all the bells and whistles if we don't want to. I've learnt, however, that some new things can be quite useful, and we get used to them rapidly!

As always, looking forward for your comments, keep them coming!

Comments

  1. Would be interesting to see what nice features (if any) Java has that c# doesn't have!

    ReplyDelete
    Replies
    1. There are a few, anonymous types coming to my mind!

      Delete
    2. Also checked exceptions - the need to declare exceptions that can be thrown by a method and are not RuntimeException-derived.

      Delete

Post a Comment

Popular posts from this blog

OpenTelemetry with ASP.NET Core

ASP.NET Core Middleware

.NET Cancellation Tokens