Untitled

 avatar
unknown
plain_text
a year ago
36 kB
6
Indexable
kotlin.oop.features

1. delegation
    1.1. BuildingDelegate.kt

import kotlin.reflect.KProperty
import kotlin.properties.ReadWriteProperty
import kotlin.properties.ReadOnlyProperty


/*
Implementing the delegate

- thisRef: it must be the same type as, or a supertype of, the class owning the delegating property. It represents the
    object that contains the delegated property and can be used to access other members of the object.
- property: it must be of type KProperty<*> and can be used to access the metadata of the delegated property.
 */

object Formatter {

    private var value: String = ""

    operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
        return value
    }

    operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
        this.value = value.replace(Regex("[aeiouAEIOU]"), "").uppercase()
    }
}

class Example {
    var firstProp: String = ""
        set(value) {
            // Remove all vowels and convert the remaining string to uppercase
            field = value.replace(Regex("[aeiouAEIOU]"), "").uppercase()
        }

    var secondProp: String = ""
        set(value) {
            // Remove all vowels and convert the remaining string to uppercase
            field = value.replace(Regex("[aeiouAEIOU]"), "").uppercase()
        }

    var thirdProp: String by Formatter
    var fourthProp: String by Formatter
}

class Delegate {
    private var curValue = ""

    operator fun getValue(thisRef: AnotherExample, property: KProperty<*>): String {
        println(property.name + ": " + thisRef.stringProp + thisRef.foo()) // thisRef allows us to access any member of the class: AnotherExample
        return curValue
    }
}

class AnotherExample {
    val stringProp: String by Delegate()
    fun foo(): String {
        return ""
    }
}

fun implementingTheDelegate() {
    val example = Example()
    example.thirdProp = "Thieu"
    println(example.thirdProp) // TH
}


/*
Anonymous delegates

Simply use the ReadOnlyProperty and ReadWriteProperty interfaces from the Kotlin standard library.
- getValue() is declared in ReadOnlyProperty.
- ReadWriteProperty extends it and adds setValue().
 */

fun anonymousDelegate() = object : ReadWriteProperty<Any?, String> {
    // we define a function called anonymousDelegate that returns an instance of an anonymous object implementing
    // the ReadWriteProperty interface for a property of type String.

    // You might have noticed that we are delegating properties directly from inside the main function and they are not
    // declared inside a class. This is another feature provided by Kotlin, and it is called local delegated properties,
    // which means you can use delegated properties not only in classes, but also as local variables within functions.

    var curValue = ""

    override fun getValue(thisRef: Any?, property: KProperty<*>): String {
        return curValue
    }

    override fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
        curValue = value
        println("The new value of ${property.name} is: $value")
    }
}


/*
Delegating to another property

In Kotlin, you can delegate a property to another property, allowing you to reuse the behavior of an existing property
for a new property.
 */

class Example2 {
    private var _counter = 0
    var counter: Int
        get() = _counter
        set(value) {
            _counter = value
            println("Counter set to $value")
        }

    var anotherCounter: Int by this::counter
}


/*
Best practices

1. Use standard delegates when possible.
2. Keep your delegated properties focused: Each delegated property should do one thing well.
3. Avoid unnecessary delegation.
 */


fun main(args: Array<String>) {
    implementingTheDelegate()

    val readOnlyString: String by anonymousDelegate()
    var readWriteProperty: String by anonymousDelegate()
    readWriteProperty = "Hello"

    val example = Example2()
    val count = example::counter

    example.counter = 10
    println(count.get()) // 10
}

#############################

    1.2. ClassDelegation.kt

package oop.features.delegation

/*
Class delegation is a mechanism in Kotlin that allows you to delegate the implementation of an interface or functionality of one class to another class.
The keyword by is used in Kotlin to delegate the implementation of an interface or functionality of one class to another class.

Differences between inheritance and delegation
1. Inheritance:
    - A class inherits the functionality and properties of the parent class.
    - There is a hard link between the classes.
    - There is a possibility of multiple inheritance problems, such as the "diamond problem".

2. Delegation:
    - The implementation of an interface or functionality is passed to another class.
    - It provides flexibility and modularity and helps avoid multiple inheritance problems.
 */

// Step 1: Creating an interface
interface Drawable {
    fun draw()
}

// Step 2: Creating a delegate class
class Circle : Drawable {
    override fun draw() {
        println("Drawing a circle")
    }
}

class Rectangle : Drawable {
    override fun draw() {
        println("Drawing a rectangle")
    }
}

// Step 3: Creating a class that uses delegation
class DrawingBoard(private val drawable: Drawable) : Drawable by drawable

fun main() {
//    var circle: Drawable = Circle()
//    var rectangle: Drawable = Rectangle()
//
//    fun draw(drawable: Drawable) = drawable.draw()
//    draw(circle)
//    draw(rectangle)

    val circle = Circle()
    val rectangle = Rectangle()

    val drawingBoard1 = DrawingBoard(circle)
    drawingBoard1.draw()
    val drawingBoard2 = DrawingBoard(rectangle)
    drawingBoard2.draw()
}

###################################

    1.3. Delegate.kt

/*
In object-oriented programming (which is the case of Kotlin), for example, the main tool for code reusage is inheritance
(and composition, consequently).

Delegation is a process of using a certain object instead of providing implementation
 */

interface MyInterface {
    fun print()
    val msg: String
}

class MyImplementation : MyInterface {
    override fun print() {
        println(msg)
    }

    override val msg: String = "MyImplementation sends regards!"
}

// when we need to use the implementation of the interface, we just reference the already existing implementation,
// and Kotlin does the rest.
class MyNewClass(
    base: MyInterface
    // Here we expect an implementation of MyInterface as a parameter (named "base")
) : MyInterface by base
    // And here we state that MyInterface is implemented by the previously obtained parameter, the one named "base"
{
    override val msg = "Delegate sends regards."

    // The code looks like this:
    // We create an instance of class, implementing MyInterface
    // val delegate = MyImplementation()
    // Then we pass this implementation instance as a parameter
    // val delegatingObj = MyNewClass(delegate)
    // println(delegatingObj.msg)

}

fun main() {
    val delegate = MyImplementation()
    val delegatingObj = MyNewClass(delegate)

    println(delegatingObj.msg) // Delegate sends regards.
    delegatingObj.print() // MyImplementation sends regards!

    // MyNewClass khong co print(), nhung co co doi tuong base la mot trien khai cua MyInterface.
    // No co print() va duoc goi khi chung ta viet delegate.print().
    // Vi vay, MyNewClass chi giao nhiem vu (delegate task) cho MyImplementation (The delegate) va in ra "MyImplementation sends regards!"


    val loggerInstance = BasicLogger()
    val dateTimeNotifier = ConsoleNotifier(loggerInstance)

    val simpleParser = ExampleParser(dateTimeNotifier, loggerInstance)
    simpleParser.start()

    // [05.11.2022-14:31:04]: OnBefore!
    // [05.11.2022-14:31:04]: Parsing...
    // [05.11.2022-14:31:04]: OnAfter!
}


// Callback and Logger example

// Defines the contract for callbacks
interface ICallbackReceiver {
    fun onBeforeAction()
    fun onAfterAction()
    fun action(function: () -> Unit) {
        onBeforeAction()
        function()
        onAfterAction()
    }
}

// Defines the contract for logging
interface ILogger {
    fun getStubDateTime() = "05.11.2022-14:31:04" // placeholder date and time

    val format: String
        get() = "[${getStubDateTime()}]: "

    fun print(s: String)
}

// Simple implementation of ILogger interface
class BasicLogger : ILogger {
    override fun print(s: String) = println(format + s)
}

// Implementation of ICallbackReceiver that uses BasicLogger for printing
class ConsoleNotifier(logger: ILogger) : ICallbackReceiver, ILogger by logger {
    val onBeforeStr = "OnBefore!"
    val onAfterStr = "OnAfter!"

    // "print" is delegated to "logger"
    override fun onBeforeAction() = print(onBeforeStr)
    override fun onAfterAction() = print(onAfterStr)
}

// Class implementing both interfaces by delegation
class ExampleParser(notifier: ConsoleNotifier, logger: BasicLogger) :
    ICallbackReceiver by notifier,
    ILogger by logger
{

    fun start() = action { parseFiles() }

    fun parseFiles() {
        print("Parsing...")
        // do some file parsing
    }
}


#################################

    1.4. StandardDelegates.kt

import kotlin.properties.Delegates

/*
In addition to class delegates, Kotlin has a robust functionality known as delegated properties.
This feature allows the delegation of getter and setter methods of a property to another object.
 */


/*
1. Overview of delegated properties

Unlike traditional properties, delegated properties are not backed by a class field. Instead, they delegate getting and
setting to another piece of code. This abstraction allows for shared functionality between similar properties.

For example:
class Example {
    var p: String by Delegate()
}

The delegate has the getValue() and setValue() methods, which take over the get() and set() methods
 */


/*
2. Standard delegates
- Lazy properties: the value is computed only during the first access.
- Observable properties: listeners are notified about changes to this property.
- Vetoable properties: allowing a lambda function to decide if a new value should be accepted or rejected.
- NotNull properties: a property delegate for a non-null property that must be initialized before it is accessed.
- Storing properties in a map: instead of using a separate field for each property, properties can be stored in a map.
 */

/*
3. Practical use cases
- Lazy initialization: it is useful for properties that are expensive to compute or that might not be needed at all.
- Observing property changes: such as updating the UI or validating the new value.
- Vetoing property changes: ensuring that a value remains within a certain range or meets certain criteria.
- NotNull properties: useful for ensuring that certain preconditions are met before an object is used.
- Storing properties in a map: useful for dynamic data structures or for serializing and deserializing objects.
 */

// Lazy properties:
fun lazyProperties() {
    // The lazy function takes a lambda and returns an instance of Lazy<T>, which serves as a delegate for implementing
    // a lazy property.
    // The first call to get() executes the lambda passed to lazy() and remembers the result. Subsequent calls to get()
    // simply return the remembered (cached) result.

    val lazyValue: String by lazy {
        print("Computed! ")
        "Hello"
    }

    println(lazyValue) // Computed! Hello
    println(lazyValue) // Hello -> Cached Value
}

// Observable properties
class User {

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

fun observableProperties() {
    // The observable delegate allows for a lambda to be triggered any time the value of the property changes,
    // resulting in change notifications or updating of other related properties.

    val user = User()
    user.rank = "first" // rank: <no rank> -> first
    println(user.rank) // first
    user.rank = "second" // rank: first -> second
    println(user.rank) // second
}

// Vetoable properties
fun vetoableProperties() {
    // The lambda function is called before a new value is set, and it allows the function to decide if the new value
    // should be accepted or rejected.

    var max: Int by Delegates.vetoable(0) {
        prop, old, new -> new > old
    }

    println(max) // 0
    max = 10
    println(max) // 10
    max = 5
    println(max) // 10
}

// NotNull properties
class Person{
    var name: String by Delegates.notNull()
}

fun nonnullProperties() {
    // notNull is a property delegate for a non-null property that must be initialized before it is accessed.

    val person = Person()
    person.name // Throws IllegalStateException:
    // Property name should be initialized before get.

    person.name = "Ahmed Omar"
    println(person.name) // Prints "Ahmed Omar"
}

// Storing properties in a map

class User2(val map: MutableMap<String, Any?>) {
    var name: String by map
    var age: Int     by map
}

fun storingPropertiesInAMap() {
    // Properties can be stored in a MutableMap or Map to back mutable or immutable properties, respectively.

    val user = User2(mutableMapOf(
        "name" to "Ahmed Omar",
        "age"  to 25
    ))

    println(user.name) // Prints "Ahmed Omar"
    println(user.age)  // Prints 25

    user.name = "Ahmed Omar"
    user.age = 30

    println(user.name) // Prints "Ahmed Omar"
    println(user.age)  // Prints 30
}

fun main() {
    observableProperties()
}

#####################################

2. ObjectExpressions.kt

/*
Object expressions in Kotlin allow creating anonymous objects for specific tasks. These objects are instances of
a class created on-the-fly and have no name.
 */

interface ClickListener {
    fun onClick()
}

class Button {
    private var clickListener: ClickListener? = null

    fun setOnClickListener(listener: ClickListener) {
        clickListener = listener
    }

    fun click() {
        clickListener?.onClick()
    }
}

fun main() {
    // objectExpressions()
    example1()
    example2()
}

fun objectExpressions() {
    val anonymousObject = object {
        val x = 10
        val y = 20

        fun printSum() {
            println("Sum: ${x + y}")
        }
    }
    anonymousObject.printSum() // Output: Sum: 30

    val clickListener = object : ClickListener {
        override fun onClick() {
            println("Button clicked!")
        }
    }
    clickListener.onClick() // Output: Button clicked!

    // Anonymous objects have a limited scope. If you want to use an anonymous object as a variable value,
    // you need to specify the variable type explicitly:
    val listener: ClickListener = object : ClickListener {
        override fun onClick() {
            println("Button clicked!")
        }
    }
    listener.onClick() // Output: Button clicked!
}

fun example1() {
    val button = Button()
    button.setOnClickListener(object : ClickListener {
        override fun onClick() {
            println("Button clicked!")
        }
    })
    button.click()
}

data class Person(val name: String, val age: Int)

fun example2() {
    val people = listOf(
        Person("Alexander", 25),
        Person("Elena", 30),
        Person("Dmitry", 21)
    )

    val sortedByAge = people.sortedWith(object : Comparator<Person> {
        override fun compare(o1: Person, o2: Person): Int {
            return o1.age.compareTo(o2.age)
        }
    })

    println(sortedByAge)
}

// In Kotlin, you can use an object instead of the classic singleton.
// However, sometimes you may need to replace a singleton with a regular class.
interface Database {
    fun connect()
}

open class MySQLDatabase : Database {
    override fun connect() {
        println("Connecting to MySQL...")
    }
}

fun example3() {
    val database: Database = object : MySQLDatabase() {
        override fun connect() {
            println("Connecting to a new database...")
        }
    }

    database.connect() // Output: Connecting to a new database...
}

######################################

kotlin.oop.generics

1. GenericAndAny.kt

/*
Generics enable us to parameterize types when defining classes (or interfaces) and methods.
Parameterized types make it possible to reuse the same code while processing different concrete types.

However, there is also another way to reuse code. If we declare a field of type Any, we can assign a value of any reference type to it.
 */


class GenericType<T>(val t: T) {
    fun get(): T {
        return t
    }
}

class NonGenericClass(val value: Any) {
    fun get(): Any {
        return value
    }
}

fun advantageOfGeneric() {
    val nonGenericInstance: NonGenericClass = NonGenericClass("abc")
    // We cannot get a string directly from the method.
    // val str: String = nonGenericInstance.get() // Compile-time error: Type mismatch
    var str: String = nonGenericInstance.get() as String // "abc"

    val instance: NonGenericClass = NonGenericClass(123)
    val string: String = instance.get() as String // throws java.lang.ClassCastException

    // Generic is no need perform an explicit type-cast, we never get a runtime exception.
    // If we do something wrong, we can see it at compile-time.

    val stringInstance: GenericType<String> = GenericType<String>("abc")

    str = stringInstance.get() // There is no type-casting here
    // val num: Int = stringInstance.get() // It does not compile

    // if the field type in the class is Any, you can easily and with no errors assign values of different types to the field.
}

fun main() {
    val stringInstance: GenericType<String> = GenericType<String>("abc")
    val anyInstance: NonGenericClass = NonGenericClass("abc")

    println(stringInstance.get())
    println(anyInstance.get())
}

######################################

2. IntroToGenericProgramming.kt

import kotlin.reflect.typeOf

/*
Generics represent parameterized types. Generic methods and classes can handle different types in the same general way.
 */


// Generic classes

class Box<T>(t: T) {

    /* Constructor accepts
    * a variable of "some type"
    * and sets it to a field */

    var value = t  // A field of "some type"
        get() = field
        set(value) {
            field = value;
        }

}


// Generic and several type parameters

class Three<T, U, V>(var first: T, var second: U, var third: V)

class Pair<T, P>(var first: T, var second: P) {
    fun changeFirst(newValue: T) {
        first = newValue
    }

    fun changeSecond(newValue: P) {
        second = newValue
    }

    fun printData() {
        println("Values:")
        println("first = $first")
        println("second = $second")
    }
}

fun genericAndSeveralTypeParameters() {
    val nameLastname: Pair<String, String> = Pair("John", "Smith")
    val nameAge: Pair<String, Int> = Pair("Kite", 18)

    nameLastname.changeFirst("John")
    nameLastname.changeSecond("Smith")

    nameAge.changeFirst("Kate")
    nameAge.changeSecond(19)

    nameLastname.printData()
    nameAge.printData()
}


// Naming convention for type parameters

//T – Type;
//S, U, V, etc. – 2nd, 3rd, 4th types;
//E – Element (often used by different collections);
//K – Key;
//V – Value;
//N – Number.


// Creating objects of generic classes

fun creatingObjects() {
    var obj1: Box<Int> = Box<Int>(123)
    var obj2: Box<String> = Box<String>("abc")

    // But if the type is a standard one like Int, String, Double, etc., you can omit the type argument, as the compiler infers it:
    obj1 = Box(123)
    obj2 = Box("abc")

    val obj3 = Three<String, Int, Int>("abc", 1, 2)
}


// Creating your own collection

class RandomCollection<T>(private val items: List<T>) {
    fun get(index: Int): T {
        return items[index]
    }

    fun length(): Int {
        return items.size
    }
}

fun creatingYourOwnCollection() {
    var nums = RandomCollection(listOf(1, 2, 3, 4))
    for (i in 0 until nums.length()) {
        print("${nums.get(i)} ") // "1 2 3 4 "
    }
}


// Getting type of a variable

fun gettingTypeOfAVariable() {
    val fromExplicitType = typeOf<Int>()
    println(fromExplicitType) // int (Kotlin reflection is not available)
}

fun main() {
    gettingTypeOfAVariable()
}

######################################

kotlin.oop.usage

1. dataclass
    1.1. DataClass.kt

/*
The Client class has 3 properties, in order to properly compare the objects (i.e., by their properties) we need to
implement equals() and hashCode() functions

Why do we need such a long piece of code just for standard stuff? That's the right question because with the data class
we can simplify it

The data class is a convenient way to organize data or to create DTOs (Data Transfer Objects).
 */

class AnotherClient(val name: String, val age: Int, val gender: String) {
    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (javaClass != other?.javaClass) return false

        other as AnotherClient

        if (name != other.name) return false
        if (age != other.age) return false
        if (gender != other.gender) return false

        return true
    }

    override fun hashCode(): Int {
        var result = name.hashCode()
        result = 31 * result + age
        result = 31 * result + gender.hashCode()
        return result
    }
}

data class Client(val name: String, val age: Int, val gender: String) {

    // 1. Chi co the tin tuong vao nhung thuoc tinh trong constructor. Moi function se khong xem xet balance vi no nam
    //    ngoai constructor
    // 2. Co the overriding moi function ngoai tru copy()
    // 3. Primary constructor phai co it nhat mot tham so va toan bo tham so phai la val hoac var

    var balance: Int = 0

    override fun toString(): String {
        return "Client(name = '$name', age = $age, gender = '$gender', balance = $balance)"
    }

    // Neu khong override ham toString() thi khi thuc hien println(bob) se in ra:
    // Client(name=Bob, age=29, gender=Male)
}

fun copyAndComponentN() {
    val bob = Client("Bob", 29, "Male")
    val john = bob.copy(name = "John")

    println(bob) // Client(name='Bob', age=29, gender='Male', balance=0)
    println(john) // Client(name='John', age=29, gender='Male', balance=0)

    // Neu khong override ham toString() thi khi thuc hien println(bob) se in ra:
    // Client(name=Bob, age=29, gender=Male)

    println(bob.name) // Bob
    println(bob.component1()) //Bob
    println(bob.age)  // 29
    println(bob.component2()) // 29
    println(bob.gender) // Male
    println(bob.component3()) // Male

    // destructuring
    val (name, age, gender) = bob
    println(name) // Bob
    println(age)  // 29
    println(gender) // Male
}


fun main() {
    copyAndComponentN()
}

####################################

    1.2. DestructureDeclarations.kt

/*
1. Basic destructuring

We can separate all variables from the class and work with them as separate objects. This feature is called a
destructuring declaration. A destructuring declaration creates multiple variables at once.
A destructuring declaration uses a componentN() operator, that returns an n-th element from the class.
 */

data class User(val name: String, val age: Int, val isAdmin: Boolean)

fun basicDestructuring() {
    val anonym = User("Anonym", 999, false)

    val (userName, userAge, isAdmin) = anonym
    println(userName)  // prints Anonym
    println(userAge)   // prints 999
    println(isAdmin)   // prints false
}


/*
2. Destructuring without data classes

Destructuring can be used without data classes as well. We simply need to define a componentN operator manually.
componentN functions work by relying on the position of each class variable.
 */

class User2(val name: String, val age: Int, val isAdmin: Boolean){
    operator fun component1(): String = name
    operator fun component2(): Int = age
    operator fun component3(): Boolean = isAdmin
}

fun checkIsAdmin(suspiciousUser: User) {
    // destructuring
    val (name, age, isAdmin) = suspiciousUser

    if (isAdmin)
        println("Have a nice day!")
    else
        println("Sorry, you should not be here.")
}


/*
3. Destructuring with lists and loops

Destructuring declarations also work with lists and loops because List is a class with the implemented componentN operator.
 */

fun withListsAndLoops() {
    val list = mutableListOf(
        User("Han", 24, true),
        User("John", 22, false),
        User("Bob", 25, true),
        User("Cruise", 28, false)
    )

    var (user1, user2, user3) = list
    // var (user1, user2, user3, user4, user5) = list // error

    // If the list has more than 3 elements, the remaining ones will not be processed and the program will continue its
    // work. In the same way, if the list has less than 3 elements, there will be an error and the program will crash.
}


/*
4. Underscoring for variables

When we start using destructuring declarations, the Kotlin compiler may warn us about unused variables in the
destructuring declaration.
The default IDE solution is to rename unused variables as "_" (underscore)
 */

fun underscoringForVariable() {
    val list = mutableListOf(
        User("Han", 24, true),
        User("John", 22, false),
        User("Bob", 25, true),
        User("Cruise", 28, false)
    )

    var (_, _, user3) = list
    var user4 = list.component4()

    println(user3)
    println(user4)
}

fun main() {
    basicDestructuring()
    withListsAndLoops()
    underscoringForVariable()
}

###################################

2. enumsandsealed
    2.1. Enum.kt

fun main() {
    val red = Rainbow.RED
    println(red.color)
    red.printFullInfo()

    println(red.name)  // RED
    println(red.ordinal)  // 0

    println(isRainbow("black"))  // false

    println(Rainbow.valueOf("RED"))  // RED

    println(3.24.toLong())
}

fun isRainbow(color: String) : Boolean {
    for (enum in Rainbow.values()) {
        if (color.uppercase() == enum.name) return true
    }
    return false
}

enum class Materials {
    GLASS, WOOD, FABRIC, METAL, PLASTIC, CERAMICS, CONCRETE, ROCK
}

enum class Rainbow(val color: String, val rgb: String) {
    RED("Red", "#FF0000"),
    ORANGE("Orange", "#FF7F00"),
    YELLOW("Yellow", "#FFFF00"),
    GREEN("Green", "#00FF00"),
    BLUE("Blue", "#0000FF"),
    INDIGO("Indigo", "#4B0082"),
    VIOLET("Violet", "#8B00FF"),
    NULL("", "");

    fun printFullInfo() {
        println("Color - $color, rgb - $rgb")
    }

    fun findByRgb(rgb: String): Rainbow {
        for (rainbow in Rainbow.values()) {
            if (rgb == rainbow.rgb) {
                return rainbow
            }
        }

        return NULL
    }
}

######################################

    2.2. Exercise.kt

fun main() {
    println(Currency.USD.ordinal)

    val color = readln()

    println(Rainbows.valueOf(color.uppercase()).ordinal + 1)

}

enum class Currency(val country: String) {
    USD("United States dollar"),
    EUR("Euro"),
    GBP("Pound sterling"),
    RUB("Russian ruble"),
    UAH("Ukrainian hryvnia"),
    KZT("Kazakhstan teenage"),
    CAD("Canadian dollar"),
    JPY("Japanese yen"),
    CNY("Chinese yuan");

}

enum class DangerLevel(private val level: Int) {
    HIGH(3),
    MEDIUM(2),
    LOW(1);

    fun getLevel(): Int {
        return level
    }
}

enum class Rainbows(val color: String) {
    RED("Red"),
    ORANGE("Orange"),
    YELLOW("Yellow"),
    GREEN("Green"),
    BLUE("Blue"),
    INDIGO("Indigo"),
    VIOLET("Violet");
}

enum class DaysOfTheWeek {
    // write here
    SUNDAY,
    MONDAY,
    TUESDAY,
    WEDNESDAY,
    THURSDAY,
    FRIDAY,
    SATURDAY
}

####################################

    2.3. SealedClass.kt

/*
1. Basic syntax

A sealed class is abstract, so it can't be instantiated. However, of course, you can extend it.
Like with normal classes, you can declare constructors, but constructors in a sealed class must be private or protected:
 */

sealed interface CustomErrors

sealed class CustomError {

    constructor(type: String) {} // protected (default)
    private constructor(type: String, code: Int) {} // private
    // public constructor() {} //  Public gives error

    //primary constructor
    sealed class CustomError(type: String)
}


/*
2. Sealed class vs enum

Basically, a sealed class is like an enum but with more flexibility. Enum constants have only one single type, while
sealed classes offer multiple instances with greater flexibility. We can conclude that an enum is used to represent
a fixed set of values, while a sealed class is a class used to represent a fixed set of subclasses of the type of the given class.

An enum can't inherit from classes of interfaces, while a sealed class can.
 */

//enum class Staff(numberOfLessons: Int)  {
//    TEACHER(2), MANAGER("Manager is managing")
//}

// That is not possible with an enum but can be done with a sealed class:
sealed class Staff {
    class Teacher(val numberOfLessons: Int) : Staff()
    class Manager(val responsibility: String) : Staff()
    object Worker : Staff()
}

open class Person {
    fun whoAmI(name: String): String {
        return "I am $name"
    }
}

sealed class Staff2 : Person() {
    class Teacher(val numberOfLessons: Int) : Staff2()
    class Manager(val responsibility: String) : Staff2()
    object Worker : Staff2()
}


fun main() {
    val worker = Staff2.Worker
    println(worker.whoAmI("Worker"))

    val teacher = Staff2.Teacher(24)


}

########################

3. ExtensionFunction.kt

/*
Extension function cho phep them cac phuong thuc moi vao lop da ton tai ma khong thay doi ma nguon cua lop do
 */

class Client(val name: String, val age: Int) {
    fun sayHello() = "Hello"
}

fun main() {
    println("Ha".repeated(5))

    val client = Client("Thieu", 24)
    println(client.getInfo())
    println(client.sayHello())  // Hello
    println(client.sayHello("Hien"))

    val a = 123
    println(a.lastDigit())

}

fun String.repeated(number: Int = 2): String {
    return if (number <= 1) {
        this
    }
    else {
        val newString = StringBuilder()
        for (i in 1..number) {
            newString.append(this)
        }
        newString.toString()
    }
}

fun Int.lastDigit(): Int {
    return this % 10
}

fun Client.getInfo() = "$name - $age"

fun Client.sayHello() = "Hi"

fun Client.sayHello(name: String) = "Hi $name"

#############################

4. PackagesAndImports.kt

import oop.basic.Patient as MyPatient

/*
1. Packages

In a package, we group classes, functions, and/or variables according to a specific use case or functionality. To define
a package, we use a package header.
A package contains one or more Kotlin files, with files linked to a package using the same package header. One file or
class can use zero or multiple packages.
If the package is not specified, the contents of such a file belong to the default package with no name.


2. Default imports

The are a number of packages that are imported into every Kotlin file by default:
kotlin.*
kotlin.annotation.*
kotlin.collections.*
kotlin.comparisons.*
kotlin.io.*
kotlin.ranges.*
kotlin.sequences.*
kotlin.text.*

Some packages are imported depending on the target platform:
JVM:
    - java.lang.*
    - kotlin.jvm.*
JS:
    - kotlin.js.*


3. Import alias

Example:
package packages

import packageone.Person
import packagetwo.Person as PersonAdavanced

fun main() {
    val person = Person("John", 30)
    val adavanced = PersonAdavanced("John", 30)
}
 */

data class Person(val name: String, val age: Int)

fun sayHello() {
    println("Hello")
}

fun main() {
    val person = oop.usage.Person("John", 30)
    oop.usage.sayHello()

    val myPatient = MyPatient()
}

#####################################

5. PairAndTriple.kt

/*
1. Pair

Pair is a simple Kotlin datatype to store two values with the same or different types in a single instance.
There doesn't need to be any relationship between the two values.
Two pairs are equal if both their components are equal.
 */

fun myPair() {
    val pair1 = Pair(1, "one")
    println(pair1) // (1, one)

    // We can use the infix function to to create a new Pair object:
    val pair2 = 1 to "one"
    println(pair2) // (1, one)

    println(pair1 == pair2) // true
}


/*
2. How to work with Pair

To work with the values, we can use first and second properties or the componentN method to extract the values from a Pair.
With Pair, we can use two special methods – toString() and toList():
- toString() returns the string representation of the Pair including its first and second values
- toList() converts the Pair into a list
Besides, we can use the copy() method to copy Pair objects and change their properties using the name of the parameter
 */

fun workWithPair() {
    val pairOne = Pair("Hi", "I am a Pair")
    val pairTwo = "Hi" to "I am another Pair"

    // Properties
    println(pairOne.first) // Hi
    println(pairOne.second) // I am a Pair

    // Methods
    println(pairTwo.component1()) // Hi
    println(pairTwo.component2()) // I am another Pair

    // toString()
    val pair = Pair("marks", listOf(8.0, 9.0, 10.0))
    println(pair) // (marks, [8.0, 9.0, 10.0]) toString() is implicit
    println(pair.toString()) // (marks, [8.0, 9.0, 10.0])

    // toList()
    println(pair.toList()) // [marks, [8.0, 9.0, 10.0]]

    // copy()
    val other = pair.copy()
    val grades = pair.copy(second = listOf(9.0, 7.0, 8.5))
}


/*
3. Triple

Triple, like Pair, is a simple Kotlin datatype that represents a triad of values with the same or different types in a single instance.
Like in the case of Pair, the type of each property of a Triple object can also be derived from the context.
 */

fun myTripe() {
    val triple = Triple(1, "A", true)
    println(triple)
}


fun main () {
    myPair()
}

####################################

6. ToString.kt

import java.awt.Point


/*
The toString() function exists specifically to represent objects as strings.
 */

fun defaultBehavior() {
    val nonString = 1.0

    println(nonString.toString())   // 1.0
    println(nonString)  // 1.0

    // for most classes, by default, toString() still returns the name of the class and the address where the object is
    // located in the memory. Usually, we want to get text information about objects in another way, so it makes sense
    // to override toString() for our data type.
}


class BerryHolder(val weight: Int) {
    override fun toString(): String {
        return weight.toString()
    }
}

open class User(val id: Int, val login: String, val email: String) {
    override fun toString(): String {
        return "User{id=$id, login=$login, email=$email}"
    }
}

fun overridingToString() {
    println(BerryHolder(10)) // 10

    val user = User(1, "uncle_bob", "rmartin@objectmentor.com")
    println(user) // User{id=1, login=uncle_bob, email=rmartin@objectmentor.com}
}


class Author(id: Int, login: String, email: String, val books: String): User(id, login, email)

fun inheritance() {
    // Another reason for overriding the toString() method is working with superclasses or parent classes.

    val user = User(1, "marys01", "mary0101@gmail.com")
    val author = Author(2, "srafael", "rsabatini@gmail.com", "Captain Blood: His Odyssey")

    println(user)   // User{id=1, login=marys01, email=mary0101@gmail.com}
    println(author) // User{id=2, login=srafael, email=rsabatini@gmail.com}
}

fun main() {
    overridingToString()
    inheritance()
}

###############################

kotlin.oop

Main.kt

import oop.basic.visibility.MyClass

fun main() {
    val myClass = MyClass() // Using the imported class
    val myInternalClass = oop.basic.visibility.MyInternalClass() // Using the internal class without importing

}
}
Editor is loading...
Leave a Comment