When it comes to sugar and fancy features in programming languages, C # and Kotlin are among the first options in mind. Since these two languages ​​occupy similar niches, that is, they are strongly typed, garbage collected, cross-platform, used both in the backend and in mobile development, today we will try to compare their syntactic capabilities and arrange a small vote. To make the comparison fair, we will consider the latest versions of both languages. I will make a reservation about my impartiality: I equally like both languages, they are in continuous development and do not lag behind each other. This article is a comparison article, not a tutorial, so some run-of-the-mill syntactic possibilities may be omitted.

Let’s start at the entry point

In C # this role is played by the static Main or top-level entry point method, for example

using static System.Console;

WriteLine("Ok");

Kotlin needs a main function

fun main() = println("Ok")

From these small two examples, first of all, it is noticeable that in Kotlin you can omit the semicolon. With a deeper analysis, we see that in C #, despite the conciseness of the indicative entry point, static methods in other files still need to be wrapped in a class and explicitly imported from it ( using static System.Console ), and Kotlin goes further and allows you to create full-fledged functions …

Declaring Variables

In C #, the type is written on the left, and the new keyword is used to create an instance. There is a special word var, which can replace the type name on the left. However, variables within methods in C # remain susceptible to re-assignment.

Point y = new(0, 0); 
var x = new Point(1, 2);
x = y; // Нормально

In Kotlin, types are written on the right, but they can be omitted. Besides var, val is also available, which does not allow re-assignment. You don’t need to specify new when instantiating.

val y: Point = Point(0, 0)
val x = Point(1, 2)
x = y // Ошибка компиляции!

Working with memory

In C #, meaningful (when used inside methods, they are placed on the stack) and reference (placed on the heap) types are available to us. This feature allows you to apply low-level optimizations and reduce memory consumption.

For objects of structures and classes, the ‘==’ operator will behave differently when comparing values ​​or references, although this behavior can be changed thanks to overloading. At the same time, some inheritance restrictions are imposed on structures.

// Значимый тип, при использовании внутри метода попадет на стек
struct ValueType {}
// ССылочный тип, будет в куче
class ReferenceType {}

As for Kotlin, it does not have any division for working with memory. Comparison ‘==’ always happens by value, for comparison by reference there is a separate operator ‘===’. Objects are almost always allocated on the heap, and only for some basic types, for example Int, Char, Double, the compiler can apply optizmisations by making them primitives jvm and placing them on the stack, which in no way affects their semantics in the syntax. One gets the impression that, on the one hand, runtime and working with memory is a stronger side of .NET in general, and on the other hand, the reference semantics in Kotlin is implemented much easier, you just write the code and don’t think about the placement / options for passing the value.

Null safety

C # (since version 8) has null protection. However, it can be explicitly circumvented using the!

var legalValue = maybeNull!;
// если legalValue теперь null, 
// то мы получим exception при первой попытке использования

In Kotlin, you need to use two exclamations to use null, but there is another difference that shows great thoughtfulness.

val legalValue = maybeNull!! 
// если maybeNull == null, 
// то мы получим exception сразу же

Class properties

In C #, a convenient abstraction is available instead of get / set methods, that is, well-known properties. At the same time, traditional fields remain available.

class Example
{   
  // Вычислено заранее и сохранено в backing field
  public string Name1 { get; set; } = "Pre-calculated expression";
  
  // Вычисляется при каждом обращении
  public string Name2 => "Calculated now";
  
  // Традиционное поле
  private const string Name3 = "Field"; 
}

In Kotlin it is simpler, there are no fields at all, only properties are available by default. At the same time, unlike C #, public is the default scope, so it is recommended to omit the corresponding keyword. To distinguish properties, with and without set, the same var / val keywords are used.

class Example {
  
  // Вычислено заранее и сохранено в backing field
  val name1 = "Pre-calculated expression"
  
  // Вычисляется при каждом обращении
  val name2 get() = "Calculated now"
}

Data classes

In C #, the word record is enough to create a class for storing data, it will have the semantics of meaningful types in comparison, but it still remains referential (it will be allocated on the heap):

class JustClass
{
  public string FirstName { init; get; }
  public string LastName { init; get; }
}

record Person(string FirstName, string LastName);

... 
  
Person person1 = new("Nancy", "Davolio");
Person person2 = person1 with { FirstName = "John" };

In Kotlin, you need to add the data keyword to the word class

class JustClass(val firstName: String, val lastName: String)

data class Person(val firstName: String, val lastName: String)

...

val person1 = Person("Nancy", "Davolio")
val person2 = person1.copy(firstName = "John")

Type extensions

In C #, such types must be in a separate static class and accept the caller as the first argument marked with this

static class StringExt
{
  public static Println(this string s) => System.Console.WriteLine(s)
    
  public static Double(this string s) => s + s
}

In Kotlin, the expandable type must be to the left of the method, which can be placed anywhere. In this case, the type can be extended not only by the method, but also by the property

fun String.println() = println(this)

fun String.double get() = this * 2

Lambda expressions

C # has a special operator => for them

numbers.Any(e => e % 2 == 0);
numbers.Any(e => 
  {
    // объемная логика ...
    return calculatedResult;
  })

In Kotlin, lambdas fit organically into the C-like syntax, in addition, in many cases, the compiler will inline their calls directly into the method used. This allows you to create efficient and beautiful DSLs (Gradle + Kotlin for example).

numbers.any { it % 2 == 0 }
numbers.any {
  // объемная логика ...
  calculatedResult
}

Terms and templates

C # has a very powerful pattern matching with conditions (example from the documentation)

static Point Transform(Point point) => point switch
{
  { X: 0, Y: 0 }                    => new Point(0, 0),
  { X: var x, Y: var y } when x < y => new Point(x + y, y),
  { X: var x, Y: var y } when x > y => new Point(x - y, y),
  { X: var x, Y: var y }            => new Point(2 * x, 2 * y),
};

Kotlin has a similar switch when expression, which, despite the possibility of pattern matching, cannot contain deconstruction and guard conditions at the same time, but thanks to the concise syntax, you can get out:

fun transform(p: Point) = when(p) {
  Point(0, 0)    -> Point(0, 0)
  else -> when {
    p.x > p.y    -> Point(...)
    p.x < p.y    -> Point(...)
    else         -> Point(...)
  }
}

Summing up 

It is almost impossible to put all the differences of both languages ​​in one article. However, we can already draw some conclusions. It is noticeable that the Kotlin-way is more about minimizing the number of keywords, implementing all the sugar on top of the basic syntax, and C # seeks to become more convenient by increasing the number of available expressions at the level of the language itself. Kotlin has the advantage that its creators can look back at the good features of C # and laconic them, and C # benefits from strong support from Microsoft and better runtime.