The Programmers Guide To Kotlin - Enums & Sealed Classes
Written by Mike James   
Monday, 26 August 2019
Article Index
The Programmers Guide To Kotlin - Enums & Sealed Classes
An Example & Sealed Classes

For example if we add a primary constructor to Days we can add a workingHours property that each of the instances initialize:

enum class Days(val workingHours:Int) {
    Monday(10), Tuesday(10), Wednesday(5), Thursday(10), 
                Friday(5), Saturday(0), Sunday(0)
}

Now each enum instance has a workingHours property:

println(myDay.workingHours)

It is occasionally useful to see other ways of doing things and so here is a secondary constructor approach to implementing workingHours:

enum class Days {
    Monday(10), Tuesday(10), Wednesday(5), Thursday(10), Friday(5), Saturday(0), Sunday(0)
    ;     val workingHours: Int     constructor(workingHours: Int) {
        this.workingHours = workingHours
    }
}

Perhaps the ultimate customization of each of the enum instances is to define each one using an anonymous class. This allows each one to have its own implementation of abstract methods.

For example we could implement workingHours as a custom function defined for each enum:

enum class Days {
    Monday {
        override fun workingHours() = 10
    },
    Tuesday {
        override fun workingHours() = 10
    },
    Wednesday {
        override fun workingHours() = 5
    },
    Thursday {
        override fun workingHours() = 10
    },
    Friday {
        override fun workingHours() = 5
    },
    Saturday {
        override fun workingHours() = 0
    },
    Sunday {
        override fun workingHours() = 0
    }
    ;     abstract fun workingHours(): Int }

Now of course we have to call workingHours as a function:

println(myDay.workingHours())

Notice that the importance of this simple example is that in principle each of the overridden functions could do its job in an entirely different way. Perhaps Friday could generate a random number for workingHours.

Sealed Classes – A Better Enum?

An enum is a set of classes associated with an ordinal, but each ordinal has only one instance of the class. Sealed classes are an attempt to be more flexible but they lack the convenience of enums – you have to do more of the work.

To show how they compare let's use a sealed class to implement the days of the week enumeration:

sealed class Days {
    class Monday : Days()
    class Tuesday : Days()
    class Wednesday : Days()
    class Thursday:Days()
    class Friday:Days()
    class Saturday:Days()
    class Sunday:Days()
}

With this definition you can use the enumeration in almost the same way, but Days is now a class and there are no static properties corresponding to the enum instances.

You can set a variable to a value of the class using:

var myDays: Days = Days.Wednesday()

just like an enum, but Days is a type not an object so to test for a value you have to use:

if(myDays is Days.Wednesday) println("it is wed")

One of the advantages of using a sealed class is that if you use a when expression the compiler will check that you have included all of the possible value. This is what the sealed modifier does for you. It tells the compiler that the sealed class only has the distinct list of subtypes and no more. This allows the compiler to check that you have created a when expression that doesn't miss any of them.

Notice that this only works for a when expression, i.e. one that returns a value, and not for a when statement that doesn't.

For example:

fun test(myDays: Days):String {
   return when (myDays) {
       is Days.Monday ->"Its monday"
       is Days.Tuesday -> "its tuesday"
       is Days.Wednesday -> "its wedensday"
       is Days.Thursday -> "its thursday"
       is Days.Friday -> "its friday"
       is Days.Saturday -> "its saturday"
       is Days.Sunday -> "its sunday"
   }
}

In this case if you leave one of the days out the compiler will flag an error. Normally you have to have an else clause in a when to make it complete.

This is all the sealed modifier does for you. If you change sealed to open and add an else clause to the when then everything works the same. This is quite a small advantage and while using classes is more flexible you have to do more of the work.

You can also define the classes that derive from a sealed class outside of the sealed class:

sealed class Days 
class Monday:Days()
class Tuesday:Days()
class Wednesday:Days()
class Thursday:Days()
class Friday:Days()
class Saturday:Days()
class Sunday:Days()

In this case you can refer to the derived classes directly without needing to mention the sealed class itself:

var myDays: Days = Wednesday()
if(myDays is Wednesday) println("it is wed")

This might lead to a more readable code but it also makes name clashes possible. All of the derived classes of a sealed class have to be defined in the same file.

If you want to add a name and ordinal property you can:

sealed class Days(val name:String,val ordinal:Int)
class Monday : Days("Monday",0)
class Tuesday : Days("Tuesday",1)
class Wednesday : Days("Wednesday",2)
class Thursday:Days("Thursday",3)
class Friday:Days("Friday",4)
class Saturday:Days("Saturday",5)
class Sunday:Days("Sunday",6)

and now you create a day using the same code but you can access name and ordinal properties:

var myDays: Days = Wednesday()
println(myDays.name)

The advantage of using a class is that now you aren't restricted to a single instance for a particular day. For example you can add a working hours property:

class Wednesday(var workingHours:Int=0):
Days("Wednesday",2)

The other days of the week would be defined in the same way. Now we can create two Wednesdays with different working hours:

var myDay1=Wednesday(10)
var myDay2=Wednesday(4)
println(myDay1.workingHours)
println(myDay2.workingHours)

and you get 10 and 4 printed. If you were using an enum, the instance that represented Wednesday could only hold a single value for workingHours. If you don't need state information of this sort then you can use objects rather than classes within sealed classes.

We could continue to explore this approach and do all sorts of clever things, but it is essentially the application of general object-oriented programming techniques and not something additional that Kotlin provides.

Sections not included in this extract:

  • Delegated Properties

  • Destructuring

Summary

  • Kotlin doesn’t provide structs or any value alternative to classes, but it does provide a data class which has data properties and a set of methods to work with them.

  • Equality is a difficult thing to define in an object-oriented world. There are two basic equality operators == for equality of reference and === for structural or content equality.

  • If you want equality to be correctly interpreted for your custom classes you need to implement your own equals method. This can either perform a shallow or a deep comparison.

  • Arrays have a referential definition of equals, but you can also use contentEquals for a shallow structural equals and contentDeepEquals for a deep structural equals.

  • Data classes, List, Map and Set have a generated shallow equals.

  • Enums allow you to construct ordinal data representations that map names to integers. An enum behaves like a static class that has properties that are instances of the same type.

  • An enum can have properties and methods but these are shared between all instances of the type.

  • Sealed classes provide an alternative to enum but you have to do more work to implement similar behavior. They work like a set of derived classes that form a known set. The compiler will check that you have included them all in a when expression.

  • Delegation is an alternative to inheritance and you can automatically delegate property implementation to a specific object that implements a delegated interface.

  • Destructuring is a simple mechanism for unpacking the data contained in a structure into individual variables.

  • The spread operator * allows you to pass an array to a vararg parameter.

 

kotlinlogo

 

This article is an extract from: 

Programmer's Guide To Kotlin Third Edition

kotlin3e360

You can buy it from: Amazon

Contents

  1. What makes Kotlin Special
  2. The Basics:Variables,Primitive Types and Functions 
  3. Control
         Extract: If and When 
  4. Strings and Arrays
  5. The Class & The Object ***NEW!
  6. Inheritance
  7. The Type Hierarchy
  8. Generics
  9. Collections, Iterators, Sequences & Ranges
        Extract: Iterators & Sequences 
  10. Advanced functions 
  11. Anonymous, Lamdas & Inline Functions
  12. Data classes, enums and destructuring
        Extract: Destructuring 
  13. Exceptions, Annotations & Reflection
  14. Coroutines
        Extract: Coroutines 
  15. Working with Java
        Extract: Using Swing
  16. Compose Multiplatform
        Extract: Compose Layout 

<ASIN:B0D8H4N8SK>

To be informed about new articles on I Programmer, sign up for our weekly newsletter, subscribe to the RSS feed and follow us on Twitter, Facebook or Linkedin.

Banner


Remembering Thomas Kurtz, Co-creator of BASIC
15/11/2024

Thomas Eugene Kurtz, the co-founder of the BASIC programming language, has died at the age of 96. BASIC, which was developed for the purpose of education, popularized computer programming making it ac [ ... ]



PHP 8.4 Adds Property Hooks
26/11/2024

PHP 8.4 is available with improvements including property hooks, asymmetric visibility, and an updated DOM API.


More News

espbook

 

Comments




or email your comment to: comments@i-programmer.info

<ASIN:1871962536>

<ASIN:1871962544>

 

Chapter 12

Data Classes, Enums & Destructuring

This chapter is about the new features that Kotlin supports to make working with data within a program easier. It isn't about database or file access, which are both largely unchanged from Java. This is about some of the lower level approaches to make representing data within your programs easier and clearer.

Data Classes

If you program in a language other than Java or Kotlin, you may be familiar with structs, structure or records to store data, sometimes along with a few simple methods. In Java and hence Kotlin there are no structs. Instead, to store data, you simply create a class that has the properties corresponding to the data you want to store.

For example:

class MyPersonClass{
var firstName:String=""
var secondName:String=""
var age:Int=0
}

This is a simple class that can be used to store a person’s name and age.

For example:

val person1=MyPersonClass()
    person1.firstName="Mickey"
    person1.secondName="Mouse"
    person1.age=89

Other languages permit the use of classes in exactly this way, so why do they have structs? The answer is for efficiency. Structs are generally value types, and this can make processing data faster. As long as the overhead of building reference-based objects from classes is low, there is no need to introduce a second entity capable of storing data.

 

A data class is just a class that does nothing but store data. Kotlin's primary constructor syntax makes it very easy to create a data class as you simply have to list the properties as val or var in the primary constructor. Thus the previous example can be written:

class MyPersonClass(
    var firstName:String="",
    var secondName:String="",
    var age:Int=0
)

This is slightly simpler and the class so created is exactly the same, but you can now initialize the properties using the primary constructor:

val person1=MyPersonClass(firstName="Mickey",
                             secondName="Mouse",age=89)

Notice that you can create private properties by putting private in front of the parameter and read-only properties by using val in place of var.

You can also include the modifier data in front of the class definition to create a Kotlin data class. In this case the properties are created as before but the compiler also generates some standard utility methods:

data class MyPersonClass(
    var firstName:String="",
    var secondName:String="",
    var age:Int=0
)

Now you also have automatically generated methods for:

  • equals – used for testing equality of content of two data classes
    The equals method is used to implement the == relational operator. This tests for equality of type and content. That is, if two instances of the data object have the same data stored in their properties, == is true. Compare this to the === operator which tests for equality of reference, i.e. do two variables reference the same object?

Equality is such an important topic that it deserves a section to itself, see below

  • hashcode – generates a hash code based on the content
    The hashcode method computes a unique hash for every object, but for a data class the hashcode is only computed using the data properties, which means two instances of the class with the same data have the same hash code.

More on this in the section on Kotlin equality.

  • componentN – access functions used in destructuring

Basically these are functions that allow other functions to access the data properties without knowing their names. They allow data classes to be used in destructuring – see later. For example:

val(firstName,secondName,age)=person1
automatically stores each of the properties in the variables on the left.

Notice that assignment is based on property order and not the names involved, see the section on destructuring later in this chapter.

  • toString – a function that uses the content to create a string

The default toString method simply consists of the name of class followed by the hash value of the instance e.g:

MyPersonClass@5e2de80c

The generated toString for a data class creates a string with each property and its current value. For example, if you don't put data in front of the class declaration then you still get inherited equals, hashcode and toString methods, but these are inherited from the Any class and are very basic – in particular they don't make use of the data properties. With a data class you get compiler generated custom methods tailored to the data in the class. For example, an instance of the MyPersonClass generates: MyPersonClass(firstName=Mickey, secondName=Mouse, age=89)

  • copymakes a copy of an instance

The copy method will create the new instance for you and set its properties to the same values as the old instance. For example to create a copy of an instance of MyPersonClass all you need is:

val person3=person1.copy()

The generated copy method has default values set to the current values of the instance being copied. For example, in the case of the MyPersonClass, copy is defined as:

fun copy(firstName:String=this.firstName,
         secondName:String=this.secondName,
    var age:Int=this.age)

This means that if you want to change any of the properties during the copy you can by providing new values that override the defaults. For example:

val person3=person1.copy(firstName="Minnie")

changes just the firstName property.

This is a technique worth remembering.

Equality

A fundamental task in programming is to work out when two things are equal. This is especially important in the case of data classes, and it is the reason the system generates a custom equals method for you.

Kotlin has two equality operators == and === and their negation != and !==.

The simpler operator is === which is a reference equality. This just tests to see if two variables are referencing the same object. For example:

val person1=MyPersonClass(firstName="Mickey",
secondName="Mouse",age=89)
val person2=MyPersonClass(firstName="Mickey",
secondName="Mouse",age=89)
println( person1 === person2 )

Prints false as even though the data classes may appear to be equal given they have the same property values, they are different objects that just happen to have the same property values.

The == operator tests for structural equality.

That is, all of the objects have to have identical properties. If this is the case then they are considered equal. Notice that the == operator is used in many other comparison operations – in, contains and so on.

The == operator is implemented by the equals function for the type being compared. If you implement your own classes and you want to compare them then you need to override the equals method that you inherit from Any.

The Kotlin documentation states that any equals method must behave like an equality operator. It should be:

  • reflexive, i.e. a==a is true

  • symmetric, i.e. if a==b is true then b==a

  • transitive, i.e. if a==b and b==c then a==c

It should also be consistent in the sense it should return the same result on repeated testing if there are no changes to the objects involved.

There is another less obvious condition. If two objects are equal then their hashCode functions should return the same hash value. What this means is that if you override equals you have to override hashCode as well.

The big problem with equals is that the default implementation inherited from Any only compares references. That is by default equals and hence === is the same as ==. Many standard classes, and all primitive data types, override equals to give you the result you would expect, but not all.

For example, for arrays the equals method is still the one inherited from Any and hence two arrays are considered equal only if the variables reference the same array object.

This is sometimes what you need, but if you really want to discover if two distinct arrays are equal based on their content then you have to use:

a contentEquals b

This is an infix operator which compares elements of each array and returns true if they are all equal. This works unless one of the element is itself an array when the elements are compared referentially.

If you want elements that are themselves arrays to be compared structurally you need to use:

a contentDeepEquals b

This compares any elements that might be arrays structurally at any level in the structure. Notice that there are deep versions of hashCode and toString.

Arrays indicate the basic problem with defining equality for general classes. Do you implement a shallow equality that simply compares properties that happen to be objects referentially or do you do a deep compare that compares such objects structurally and so on.

In the case of data classes, List, Map and Set, Kotlin performs a shallow structural comparison. For an array the order of the elements is important. An array with the same values as another but in a different order is not equal. If two maps have the same set of key/value pairs then they are equal irrespective of the order they were added. For two sets to be equal they simply have have the same elements in any order. 

The notion of what constitutes equality is a very varied idea. For example is 123 equal to "123"? In most case you need to check any inherited or standard implementation of equals you may have, and see that it fits in with what you mean by equality. You also need to be ready to implement your own equals and hashCode methods.

Enums

Enums or enumerations are a way of creating ordinal data types.

That is, an ordinal data type is one that is essentially a set of named integer values. For example, a classic use of enum is to represent the days of the week as something that looks like a set of named constants Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday instead of 0,1,2,3,4,5,6. Behind the scenes of an enum there are a set of integers which identify the particular constant and provide an order relation. That is Monday<Tuesday because 0<1. The integer associated with an enum is generally called its ordinal or ordinal position.

In both Java and Kotlin, but not in many other languages, the enum data type is a class and this provides lots of additional features and possibilities. You declare an enum class by putting the enum modifier in front of a fairly standard class declaration:

enum class Days{Monday, Tuesday, Wednesday, Thursday, 
                                  Friday, Saturday, Sunday}

Notice that the enumeration of names is presented as if it was the body of the class being defined and in a sense it is. Each of the names is converted into a property of the Days enum class which in turn behaves like a static class and a type.

For example to declare a variable suitable for holding values from the enumeration you would use:

var myDay:Days

and to store a value in myDay:

myDay=Days.Monday

You can use an enum in conditionals and when statements to make your code more readable. For example:

var myDay:Days=Days.Monday     
if(myDay==Days.Monday)

If myDay is of type Days what type is Days.Monday?

The answer is that it is of type Days as well. The peculiar thing about an enum is that it is a static class that has properties that are instances of the same type. This observation is the key to understanding, and getting the most out, of enums.

The instances of the enum come with few standard methods, the most important of which are name and ordinal. As you can guess, name returns the string that is the name of the instance, and ordinal, an integer indicating its position in the enumeration sequence.

Notice that there is only one instance of each of the enum classes and this is shared between all of the variables that make use of it. That is:

var myDay=Days.Monday

and:

var yourDay=Days.Monday

both reference the same instance of Days i.e. Days.Monday. This can be important to know if you extend the enum class.

As Kotlin allows an enum class to be declared with properties and methods just like any other class, you can now see that you can extend what an enum can do.

For example, we can add a method to each of the days of the week to indicate if it is working or non-working. However, we have a small syntax problem. How do you indicate when the list of enum properties has ended and the rest of the class declaration starts? The Kotlin answer is that you use a single semicolon to mark the two sections:

enum class Days {Monday, Tuesday, Wednesday, Thursday,
                          Friday, Saturday, Sunday
;     fun isWorking() {
        if (this.name == "Saturday" || this.name == "Sunday") {
            println("non-working")
        } else {
            println("working")
        }     }
}

With this definition each of the enum properties has a new method isWorking:

var myDay: Days = Days.Saturday
myDay.isWorking()

which prints non-working.

Notice that this method is an instance method and not part of the static myDays object. This, however, does have some predefined methods:

  • values – returns an array of all of the enum values

For example:

println(Days.values()[1].name)

displays "Tuesday".

  • valuesOf(string) - returns the enum object that has a name that exactly matches the string

For example:

println(Days.valueOf("Monday").ordinal)

prints 0.

Not only can you add methods and properties to each instance, you can also define a constructor.

 

For example if we add a primary constructor to Days we can add a workingHours property that each of the instances initialize:

enum class Days(val workingHours:Int) {
    Monday(10), Tuesday(10), Wednesday(5), Thursday(10), 
                Friday(5), Saturday(0), Sunday(0)
}

Now each enum instance has a workingHours property:

println(myDay.workingHours)

It is occasionally useful to see other ways of doing things and so here is a secondary constructor approach to implementing workingHours:

enum class Days {
    Monday(10), Tuesday(10), Wednesday(5), Thursday(10), Friday(5), Saturday(0), Sunday(0)
    ;     val workingHours: Int     constructor(workingHours: Int) {
        this.workingHours = workingHours
    }
}

Perhaps the ultimate customization of each of the enum instances is to define each one using an anonymous class. This allows each one to have its own implementation of abstract methods.

For example we could implement workingHours as a custom function defined for each enum:

enum class Days {
    Monday {
        override fun workingHours() = 10
    },
    Tuesday {
        override fun workingHours() = 10
    },
    Wednesday {
        override fun workingHours() = 5
    },
    Thursday {
        override fun workingHours() = 10
    },
    Friday {
        override fun workingHours() = 5
    },
    Saturday {
        override fun workingHours() = 0
    },
    Sunday {
        override fun workingHours() = 0
    }
    ;     abstract fun workingHours(): Int }

Now of course we have to call workingHours as a function:

println(myDay.workingHours())

Notice that the importance of this simple example is that in principle each of the overridden functions could do its job in an entirely different way. Perhaps Friday could generate a random number for workingHours.

Sealed Classes – A Better Enum?

An enum is a set of classes associated with an ordinal, but each ordinal has only one instance of the class. Sealed classes are an attempt to be more flexible but they lack the convenience of enums – you have to do more of the work.

To show how they compare let's use a sealed class to implement the days of the week enumeration:

sealed class Days {
    class Monday : Days()
    class Tuesday : Days()
    class Wednesday : Days()
    class Thursday:Days()
    class Friday:Days()
    class Saturday:Days()
    class Sunday:Days()
}

With this definition you can use the enumeration in almost the same way, but Days is now a class and there are no static properties corresponding to the enum instances.

You can set a variable to a value of the class using:

var myDays: Days = Days.Wednesday()

just like an enum, but Days is a type not an object so to test for a value you have to use:

if(myDays is Days.Wednesday) println("it is wed")

One of the advantages of using a sealed class is that if you use a when expression the compiler will check that you have included all of the possible value. This is what the sealed modifier does for you. It tells the compiler that the sealed class only has the distinct list of subtypes and no more. This allows the compiler to check that you have created a when expression that doesn't miss any of them.

 

Notice that this only works for a when expression, i.e. one that returns a value, and not for a when statement that doesn't.

For example:

fun test(myDays: Days):String {
   return when (myDays) {
       is Days.Monday ->"Its monday"
       is Days.Tuesday -> "its tuesday"
       is Days.Wednesday -> "its wedensday"
       is Days.Thursday -> "its thursday"
       is Days.Friday -> "its friday"
       is Days.Saturday -> "its saturday"
       is Days.Sunday -> "its sunday"
   }
}

In this case if you leave one of the days out the compiler will flag an error. Normally you have to have an else clause in a when to make it complete.

This is all the sealed modifier does for you. If you change sealed to open and add an else clause to the when then everything works the same. This is quite a small advantage and while using classes is more flexible you have to do more of the work.

You can also define the classes that derive from a sealed class outside of the sealed class:

sealed class Days 
class Monday:Days()
class Tuesday:Days()
class Wednesday:Days()
class Thursday:Days()
class Friday:Days()
class Saturday:Days()
class Sunday:Days()

In this case you can refer to the derived classes directly without needing to mention the sealed class itself:

var myDays: Days = Wednesday()
if(myDays is Wednesday) println("it is wed")

This might lead to a more readable code but it also makes name clashes possible. All of the derived classes of a sealed class have to be defined in the same file.

If you want to add a name and ordinal property you can:

sealed class Days(val name:String,val ordinal:Int)
class Monday : Days("Monday",0)
class Tuesday : Days("Tuesday",1)
class Wednesday : Days("Wednesday",2)
class Thursday:Days("Thursday",3)
class Friday:Days("Friday",4)
class Saturday:Days("Saturday",5)
class Sunday:Days("Sunday",6)

and now you create a day using the same code but you can access name and ordinal properties:

var myDays: Days = Wednesday()
println(myDays.name)

The advantage of using a class is that now you aren't restricted to a single instance for a particular day. For example you can add a working hours property:

class Wednesday(var workingHours:Int=0):Days("Wednesday",2)

The other days of the week would be defined in the same way. Now we can create two Wednesdays with different working hours:

var myDay1=Wednesday(10)
var myDay2=Wednesday(4)
println(myDay1.workingHours)
println(myDay2.workingHours)

and you get 10 and 4 printed. If you were using an enum, the instance that represented Wednesday could only hold a single value for workingHours. If you don't need state information of this sort then you can use objects rather than classes within sealed classes.

We could continue to explore this approach and do all sorts of clever things, but it is essentially the application of general object-oriented programming techniques and not something additional that Kotlin provides.

Delegated Properties

Properties are the way data is represented in an object-oriented language. Each class has the responsibility for creating get and set functions to allow the outside world to modify and access the data. Some types of property are more sophisticated than a simple set and get with backing variables.

For example, a property might keep a history of its previous values or it might call registered callbacks when its value changes – observable properties and so on. You can implement these mechanisms on a class-by-class basis but this isn't particularly efficient, and if you want to change the way a property works you will have to go round all of the classes and change them all.

Kotlin provides a way to delegate property implementation to another class. It also allows you to delegate a local variable in a function which isn't a property in the same way:

 

For example to delegate a property:

class myClass {
 var myProperty:Int by myDelegate()
}

and to delegate a local variable:

fun mFunction(){
        var myLocalVar:Int by myDelegate()
        println(myLocalVar)
    }

In this case the delegate is an instance of the class myDelegate. This has to provide a get and set function with a particular signature. The get is:

operator fun getValue(thisRef:Any?,property:KProperty<*>):T

and the set is:

operator fun setValue(thisRef:Any?,property:KProperty<*>,value:T)

where thisRef is the object doing the delegating, property is an object that represents the property, value is the value to be set and T is the type of the property. Notice that for a local variable Any will be set to null as there is no object doing the delegating. We also need to add:

import kotlin.reflect.KProperty

to use Kproperty.

So in the case of our myClass example with an Int parameter, myDelegate might be:

class myDelegate{
operator fun getValue(thisRef:Any?, property:KProperty<*>):Int{
    return 1
}
operator fun setValue(thisRef:Any?,property:KProperty<*>,value:Int) {
}
}

where for simplicity of the example, the get returns 1 and the set throws the value away.

If you want to make sure the get and set are correct or want the system to generate stubs simply implement one of the standard interfaces ReadOnlyProperty<R,T> or ReadyWriteProperty<R,T> where R is the type of thisRef and T is the type of the property.

If we try to use the property just defined:

val myObject=myClass()
println(myObject.myProperty)

You will see 1 printed. Notice that the system has created an instance of myDelegate to use with myObject. Every time you create an instance of myObject you get a new instance of myDelegate to work with it.

For example, if we change the delegate so that a backing property is used to implement the functioning of a standard property:

class myDelegate{
  private var backing:Int=0
  operator fun getValue(thisRef:Any, property:KProperty<*>):Int{
     return backing
  }
  operator fun setValue(thisRef:Any,property:KProperty<*>,value:Int) {
    backing=value
  } }

then you can see that each instance of myClass gets it own instance of myDelegate:

val myObject1=myClass()
val myObject2=myClass()
myObject1.myProperty=1
myObject2.myProperty=2
println(myObject1.myProperty)
println(myObject2.myProperty)

The final print statements print 1 and 2 respectively, showing that the instances don't share the property.

You can pass data including lambdas.

One of the best examples of delegation is the Observable which is one of the three standard delegated properties that Kotlin provides – Lazy and Map being the other two.

The observable delegate accepts two parameters. The first is the initial value of the delegated property and the second is a function to be executed whenever the property changes. This function has the signature prop, old, new which give the property being changed, and its old and new values.

For example:

class myClass {
    var name: String by Delegates.observable("<no name>") {
        prop, old, new -> println(new)
    }
}

 

To make this work you have to import the Delegates package:

import kotlin.properties.Delegates

The delegated property is used in the same way as any other, but with the side effect that it prints the new value:

val myObject=myClass()
myObject.name="Mickey"
myObject.name="Minnie"

You will see Mickey and Minnie printed.

Taking the customization of the delegate object even further you can define your own provideDelegate operator. When you create an instance of a class that uses a delegate property then the system automatically creates an instance of the delegate class for you. If you need to, you can do the job yourself by defining the provideDelegate operator as a member or extension function of the delegate class. The provideDelegate is called to create the instance of the delegate class. It receives the same parameters as the get function i.e. thisRef and property, and it has to return an instance of the delegate.

For example to add the provideDelegate operator to our trivial example from earlier:

class myDelegate {
  operator fun provideDelegate(thisRef: MyClass, prop: KProperty<*>): myDelegate {
        println("creating delegate")
        return myDelegate()
    } private var backing: Int = 0
    operator fun getValue(thisRef: Any,
property: KProperty<*>): Int {
        return backing
    }     operator fun setValue(thisRef: Any,
property: KProperty<*>, value: Int) {
        backing = value
    } }

Now when we create an instance you will see “creating delegate" printed: 

class MyClass {
    var myProperty: Int by myDelegate()
}
fun main(args: Array<String>) {
    val myObject = MyClass()
    myObject.myProperty = 1
    println(myObject.myProperty) }

Of course, in practice the provideDelegate operator can do whatever it needs to check the validity of the delegation and to build a custom object to do the job.

You don’t need to know how delegate properties work, but it isn't complicated. When you declare a delegate property the system creates a hidden property with the name propertyname$delegate, which is a reference to an instance of the delegate class this:

private val propertyname$delegate=MyDelegate()

The generated get and set for the property simply hands off to the instance of the delegate class, e.g:

get()=propertyname$delegate.getValue(this,this::propertyname)
set(value:type)=propertyname$delegate.getValue(this,
                                    this::propertyname,value)

Once you have seen a delegate property in action you should be able to generalize and take the idea in whatever direction you need it to go.

Destructuring

Destructuring is a sophisticated sounding name for a very simple idea. Sometimes you have a data structure with a number of elements or properties and you want to unpack these into a set of standalone variables. 

For example, if we have a data class:

data class MyDataClass(var name:String,var age:Int)

Then we can create an instance and use destructuring to pack the data into separate variables:

var myDataObject=MyDataClass("Mickey",89)
var (myName,myAge) = myDataObject
println(myName)
println(myAge)

The destructuring assignment:

var (myName,myAge) = myDataObject

is converted into:

var myName = myDataObject.component1()
var myAge = myDataObject.component2()

For a data object the operator methods component1, component2 and so on are automatically generated. This is the reason why if you take the data modifier from the start of the declaration you will get an error message as the class no longer has componentN methods.

 

Of course, you can add them manually:

class MyDataClass(var name:String,var age:Int){
   operator fun component1()=name
   operator fun component2()=age
}

In fact any class that has componentN operator methods can be used in a destructuring operation.

As well as assignment destructuring can be used in for loops.

For example:

val myList=listOf(MyDataClass("Mickey",89),MyDataClass("Minnie",88))
    for ((name,age) in myList){
        println(name)
        println(age)
    }

As long as each element that the iterator returns supports destructuring, you can use this sort of for loop.

Destructuring can also allow a function to seem to return multiple values.

For example:

fun myFavouriteMouse():MyDataClass{
     return MyDataClass("Mickey",89)
 }

and you can now write:

var (myName,myAge)=myFavouriteMouse()

which looks as if myFavouriteMouse returns multiple values in the same way that you can in Python.

If you don't want to make use of a destructure value you can use an underscore to leave it out:

var (_,myAge)=myFavouriteMouse()

Finally, you can use destructuring in parameters of lambdas as long as the parameter type has componentN operator functions. Notice that this makes adding an extra set of parenthesis in a lambda an error. That is:

{ a,b-> statements that use a and b}
{(a,b)-> statements that use a and b}

are different in that the second one accepts a single parameter that can be destructured to give a and b.

 

For example:

 val myLambda= {(name,age):MyDataClass->
       println(name)
       println(age)
    }

This looks like a lambda that would accept two parameters, but because of destructuring it accepts a single instance of MyDataClass:

val myDataObject=MyDataClass("Mickey",89)
myLambda(myDataObject)

This works with any class that supports componentN operator methods.

The Spread Operator

There is one special type of destructuring designed to make it easier to use varargs. If you pass parameters to a vararg one by one it builds an array for you to hold them.

What if you already have the arguments in an array?

This is where the spread operator * comes in. It allows you to pass an array directly to the varargs as an array. You can think of it as unpacking the array into separate parameters which is why it can be regarded as a destructuring operator.

For example:

val a=arrayOf(1,2,3)
val list =asList(*a)

The asList method expects a vararg of the individual elements that will be used to create the list. The spread operator passes the array as if it was a set of individual parameters.

Summary

  • Kotlin doesn’t provide structs or any value alternative to classes, but it does provide a data class which has data properties and a set of methods to work with them.

  • Equality is a difficult thing to define in an object-oriented world. There are two basic equality operators == for equality of reference and === for structural or content equality.

  • If you want equality to be correctly interpreted for your custom classes you need to implement your own equals method. This can either perform a shallow or a deep comparison.

  • Arrays have a referential definition of equals, but you can also use contentEquals for a shallow structural equals and contentDeepEquals for a deep structural equals.

  • Data classes, List, Map and Set have a generated shallow equals.

  • Enums allow you to construct ordinal data representations that map names to integers. An enum behaves like a static class that has properties that are instances of the same type.

  • An enum can have properties and methods but these are shared between all instances of the type.

  • Sealed classes provide an alternative to enum but you have to do more work to implement similar behavior. They work like a set of derived classes that form a known set. The compiler will check that you have included them all in a when expression.

  • Delegation is an alternative to inheritance and you can automatically delegate property implementation to a specific object that implements a delegated interface.

  • Destructuring is a simple mechanism for unpacking the data contained in a structure into individual variables.

  • The spread operator * allows you to pass an array to a vararg parameter.



Last Updated ( Monday, 26 August 2019 )