BLOG
21 November 2022

Kotlin vs Swift - How do They Compare? Part One.

tech

According to the latest stackoverflow survey Swift and Kotlin are among the most-loved programming languages. Both are becoming more and more popular and have almost completely replaced the venerable Java and Objective C in the world of mobile platforms. It’s not a surprise, as the giants of the IT industry such as Apple, JetBrains and Google are involved in their development. If you are an Android or iOS developer, you likely know at least one of these languages.

This article summarizes the most significant syntax differences between the languages. Knowing them allows you to switch to a second language and become a Swifter if you are a Kotliner, and vice versa. The article will not, however, explain differences in memory management or between iOS and Android platforms.
 

At the time of writing, Swift is in version 5.7 and Kotlin version is 1.7.20. 
Some code snippets are incomplete or lack input validation for the sake of simplicity.
 
Ok, lets get started.

Keywords


The majority of the keywords are interchangeable and have the same meaning. For example, 'if', 'else', 'for', 'while', and 'continue' statements work in the same way as they do in other C-like languages.
 
Part of the reason for keyword differences is that the applications written in the languages use different memory models. Swift keywords like 'weak' and 'unowned' aid in resolving retain cycles, which Kotlin programmes do not have.

Access level (visiblity modifiers)

Both languages support access levels such as 'public,' 'internal,' and 'private.' The underlying meaning is the same. Swift supports the 'fileprivate' access level but not the 'protected' access level.

Functions

Kotlin functions are declared using the ‘fun’ keyword whereas Swift functions are declared using the ‘func’ keyword. To declare a return type, Kotlin uses colon (:) notation, Swift arrow (->). 

fun compute(): Double //Kotlin
func compute() -> Double //Swift

If a function does not return any value, the return notation along with the type declaration can be omitted. Both languages support named arguments and default arguments values.

1 2 3 4 5 6 7 8 //Kotlin fun export(sourceFileName: String = "input.txt", destinationFileName: String = "output.xlsx") { //returns nothing (Unit) //... } export(sourceFileName = "in.txt", destinationFileName = "out.xlsx") export("in.txt", "out.xlsx")

Labels make using functions in Swift more natural.

1 2 3 4 5 6 7 //Swift func export(from sourceFileName: String = "input.txt", to destinationFileName: String = "output.xlsx") { //returns nothing (Void) //... } export(from: "in.txt", to: "out.xlsx")//obligatory argument labels `from`, 'to' export()

If you don’t want an argument label for a parameter, type an underscore '_'  instead of an explicit argument label for that parameter.

1 2 3 4 func printString(_ str: String = "bar") { print(str) } printString("hello")

Static methods

In Kotlin class methods are created inside the 'companion object' block. There is no the 'static' keyword in Kotlin.

1 2 3 4 5 6 7 8 9 10 class Json { //elements declared inside companion object block are class methods companion object { fun parse(jsonCandidate: String): Json { //... return json } } } val json = Json.parse("...")

Swift takes a bit more conservative approach. The 'static' and 'class' keywords are used to utilize class functionality without creating an instance. Subclasses can override 'class' methods but they can not override 'static' methods.

1 2 3 4 5 6 7 8 9 10 11 12 13 class Json { static func parse1(_ jsonCandidate: String) -> Json { ///... return json } class func parse2(_ jsonCandidate: String) -> Json { ///... return json } } let json1 = Json.parse1("...") let json2 = Json.parse2("...")

Static variables

In Kotlin, a class variable, just like a class function, has to be declared inside 'companion object' block.

1 2 3 4 5 6 //kotlin class Counter { companion object { var globalCounter: Int = 0 } }

To declare a class variable in Swift, declare it as a normal variable, prefixed with the 'static' keyword.

1 2 3 4 //swift struct Counter { static var globalCounter: Int = 0 }

Parentheses

Usage of parentheses for contional statements, loops, 'guard', 'switch' is not required in Swift language. In Swift, parentheses are considered redundant, causing unnecessary syntactic noise. Linters (such as SwiftLint) will warn you if you include redundant parentheses.

1 2 3 4 //Kotlin if (a > b) { //Parentheses obligatory print("$a is grater than $b") }
1 2 3 4 //Swift if a > b { //Parentheses omitted print("\(a) is greater than \(b)") }

Inline if

There is no ternary operator in Kotlin. Really. Fortunately, 'if' is an expression: it returns a value. Hence, the way Kotlin declares an inline if is also expressive

val res = if (a>b) a else b

Swift supports ternary operator like most languages do.

let res = a>b ? a : b

String interpolation

In Kotlin, use the dollar sign ($) before a variable name to interpolate a value of this variable into a string.

1 2 3 val name = "Joe" println("Hello, $name") println("Your name is ${name.length} characters long")

In Swift wrap the variable in a pair of parentheses, prefixed by a backslash.

1 2 3 let name = "Joe" print("Hello, \(name)") print("Your name is \(name.count) characters long")

 

Mutability

In Kotlin, mutability means that a 'var' variable can be reassigned with another value. Variables declared with the 'val' keyword are immutable and can not be reassigned.

1 2 3 4 5 var a = 1 a = 2 //`var` variable can be reassigned val b = 1 //b = 2, doesn't compile, `val` variable can not be reassigned

In Kotlin, if a class variable is declared with the 'var' keyword, we can reassign it, even if an instance of the class is declared immutable.

1 2 3 4 5 data class User(val name: String, //immutable `val` var email: String)//mutable `var` val user = User("John Doe", "john.doe@gmail.com") //immutable user variable user.email = "john.doe@hotmail.com" //mutable user.email variable

Kotlin distinguishes mutable and immutable collection classes. Mutable collections allow adding and removing items, immutable collections are fixed-size.

1 2 3 4 5 6 7 8 val mutableNumbers: MutableList<Int> = mutableListOf(1,2,3) mutableNumbers.removeAt(0) mutableNumbers.add(0, 1) var immutableNumbers: List<Int> = listOf(1,2,3) //immutableNumbers.removeAt(0) //doesn't compile, method unavailable //immutableNumbers.add(0, 1) //doesn't compile, method unavailable immutableNumbers = mutableNumbers //`var` can be reassigned

In Swift, an immutable variable cannot be reassigned, and the variable value can not be changed. 

Declare mutable variables with the 'var' keyword and immutable with the 'let' keyword.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 var a = 1 a = 2 let b = 1 //b = 2, doesn't compile - cannot assign to value: 'b' is a 'let' constant struct User { let name: String //immutable `let` var email: String //mutable `var` } let letUser = User(name: "John Doe", email: "john.doe@gmail.com") //letUser.email = "john.doe@hotmail.com" //doesn't compile - cannot assign to property: 'user' is a 'let' constant var varUser = User(name: "John Doe", email: "john.doe@gmail.com") varUser.email = "john.doe@hotmail.com" var mutableNumbers: [Int] = [1,2,3] mutableNumbers.remove(at: 0) mutableNumbers.insert(1, at: 0) let immutableNumbers: [Int] = [1,2,3] //immutableNumbers.remove(at: 0) // doesn't compile - cannot use mutating member on immutable value: 'immutableNumbers' is a 'let' constant //immutableNumbers.insert(1, at: 0) // doesn't compile - cannot use mutating member on immutable value: 'immutableNumbers' is a 'let' constant

Loops

Both languages support  'for' and 'while' loops. The 'do' keyword in Swift is reserved for beginning of an exception-handling block, so the following loop construct:

do { } while condition //not supported in Swift

is not supported in Swift, but you can use the `repeat` instead:

repeat { } while condition //supported in Swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 for (i in 0..2) { println(i) //0, 1, 2 } var i = 0 do { println(i)//0, 1 i += 1 } while (i<2) repeat(2) { println(it)//0, 1 } for (i in 0..4 step 2) { println(i)//0, 2, 4 } for (i in 4 downTo 0 step 2) { println(i)//4, 2, 0 }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 for i in 0...2 { print(i) //0, 1, 2 } var i = 0 repeat { print("i: \(i)") i += 1 } while i<2 for i in stride(from: 0, to: 4, by: 2) { print(i)//0, 2 } for i in stride(from: 0, through: 4, by: 2) { print(i)//0, 2, 4 } for i in stride(from: 2, through: 0, by: -0.5) { print(i)//2.0, 1.5, 1.0, 0.5, 0.0 }

 

Error handling

In Kotlin, errors are represented by instances of classes that extends the 'Throwable' class. Kotlin does not have checked exceptions, functions are not denoted with the 'throws' keyword. The well known 'try/catch/finally'  block is the way to handle exceptions.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 sealed class DomainError: Exception() data class NoSuchProduct(val id: Int): DomainError() object TooManyItems: DomainError() fun sendOrder() { //... throw TooManyItems() //... } try { sendOrder() } catch (err: NoSuchProduct) { print("Product with id ${err.id} not found") } catch (err: DomainError) { print("Domain error: $err") } catch (err: Throwable) { print("Unknown error: $err") } finally { //clean up section }

In Swift, errors are represented by values of types that conform to the Error protocol. If a function calls other throwing function, the calling function has to catch an error or rethrow it. Throwing functions are marked with the 'throws' keyword. Swift does not have the 'finally' block. You can run cleanup actions using the defer statement.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 enum DomainError: Error { case NoSuchProduct(id: Int) case TooManyItems } func sendOrder() throws { //throwing method ... throw DomainError.TooManyItems //... } do { try sendOrder() } catch DomainError.NoSuchProduct(let id) { print("Product with id: \(id) not found") } catch let err as DomainError { print("Domain error: \(err)") } catch { print("Unknown error: \(error)") }

Flow control with 'switch' statement

Kotlin does not have the traditional 'switch-case' statement. Kotlin has the 'when' statement.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 sealed interface Error {} sealed class NetworkError: Error { object NoData: NetworkError() data class HttpStatusNok(val statusCode: Int): NetworkError() } enum class ParseError(val code: Int): Error { UnknownParseError(10), BadFormat(20), MissingClosingTag(30) } //`when` with object matching val result: Error = NetworkError.HttpStatusNok(statusCode = 500) when (result) { is NetworkError.NoData -> println("No data") is NetworkError.HttpStatusNok -> println("Status not ok: ${result.statusCode}") ParseError.MissingClosingTag -> println("Missing closing tag") is ParseError -> println("Parse error") } //`when` with numbers when (1.1) { 0.0 -> println("0.0") 1.1 -> println("1.1") in 2.0..9.0 -> { println("Between 2.0 and 9.0") } else -> println("Other") }

Swift’s switch statement is more powerful than its counterpart in many C-like languages.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 enum NetworkError: Error { case noData case httpStatusNok(statusCode: Int) } enum ParseError: Int, Error { case unknown = 10 case badFormat = 20 } //`switch` with object matching let error: Error = NetworkError.httpStatusNok(statusCode: 500) switch error { case NetworkError.httpStatusNok(statusCode: 500): print("Internal server error 500") case NetworkError.httpStatusNok(statusCode: let statusCode): print("Server error, status code: \(statusCode)") case is NetworkError: print("Network error") case let e as ParseError where e.rawValue == 20: print("Parse error bad format, code: \(e.rawValue)") case let e as ParseError: print("Parse error, code: \(e.rawValue)") default: print("Other error") } //`switch` with numbers switch 1.1 { case 0: print("Zero") case 1.1: print("1.1") case 2...9: print("Between 2 and 9") default: print("Other") }

Null safety

Both languages support nullable types AKA optionals. Declaration of an optional variable is identical for Kotlin and Swift.

var str: String? = null //Kotlin
1 var str: String? = nil //Swift

However, there are minor differences for using optionals. Check the examples below to understand how to use optionals in the languages.

Forced optional unwrapping

1 2 val name: String? = "Kotlin" print(name!!) //double !!
1 2 let name: String? = "Swift" print(name!) //single !

Null/nil fallback

Kotlin has the ??: operator, known as Elvis operator.

1 2 3 4 5 fun findString(): String? fun defaultString(): String val nonNilString1: String = findString() ?: defaultString() val nonNilString2: String = findString() ?: throw RuntimeException("String not found")

Swift has ?? nil coalescing operator.

1 let nonNilString: String = findString() ?? defaultString()

Unwrapping with conditions

Kotlin has a cool feature called smart cast. When an immutable optional is verified as non null, it becomes a non nullable variable.

1 2 3 4 5 6 7 8 9 10 11 12 13 val str: String? = ... if (str != null) { val strLen: Int = str.length //Smart cast in action: str is not null here println(strLen) } str?.let { print(it) //implicit 'it' variable holds a non null value } str?.let { nonNullStr -> print(nonNullStr) //explicit `nonNullStr` variable holds non null value }

Swift:

1 2 3 4 5 6 7 8 9 10 11 //swift var str: String? = ... if let nonNullStr = str { //`nonNullStr` variable holds a non nil value print(nonNullStr) } if let str = str { let strLen: Int = str.count //str is not null here print(strLen) }

Checking pre-conditions with guard clauses

Both languages offer one-liner statements used to validate input against null variables.  
In Kotlin, use Elvis operator.

1 2 val str: String? = findString() val nonNullStr = str ?: return

In Swift, guard statement is used to transfer program control out of a scope if one or more conditions aren’t met.

1 2 let str: String? = findString() guard let nonNilStr = str else { return }

Lambdas

Both languages support higher-order functions and lambda statements. There are minor differences between the languages especially in naming implicit variables. Let's look into the examples.

1 2 3 4 5 6 7 8 9 10 11 12 13 fun sum(integers: List<Int>, transformation: (Int)->Int): Int { var result = 0 integers.forEach { //implicit 'it' variable holds current value result += transformation(it) } return result } val multiplyBy2: (Int)->Int = { arg -> arg*2 } //explicit variable named arg val multiplyBy3: (Int)->Int = { it * 3 } //implicit variable 'it' val result1 = sum(listOf(1,2,3), multiplyBy2) val result2 = sum(listOf(1,2,3)) { //trailing closure it * 2 }
1 2 3 4 5 6 7 8 9 10 11 12 13 func sum(_ integers: [Int], _ transformation: (Int)->Int)-> Int { var result = 0 integers.forEach { //implicit '$0' variable holds current value result += transformation($0) } return result } let multiplyBy2: (Int)->Int = { arg in arg*2 } //explicit variable named arg let multiplyBy3: (Int)->Int = { $0 * 3 } let result1 = sum([1,2,3], multiplyBy2) let result2 = sum([1,2,3]) { //trailing closure $0 * 2 //implicit first arg '$0' }

Ranges

Kotlin has closed ranges:

1 val range = 0..3 //contains values: 0, 1, 2, 3

Ranges in Swift are more advanced:

1 2 3 4 5 6 7 8 9 //Swift supports closed ranges let closedRange = 0...3 //contains values: 0, 1, 2, 3 //half-open ranges let halfOpenRange = 0..<3 //contains values: 0, 1, 2 //one-sided ranges let oneSidedRange1 = ..<3 //contains values: from -∞ to 2 let oneSidedRange2 = 2... //contains values: 2 to ∞

This is everything for now, but in the second part of the article we will take a closer look at the differences between abstractions, classes and inheritance. I hope you find the article useful and will come back for part two!



Author
Robert Andrzejczyk
iOS Developer