Types de fonctions

Comme en Python, les fonctions en Kotlin sont des valeurs de première classe - elles peuvent être affectées à des variables et passées en tant que paramètres. Le type d'une fonction est un type de fonction, qui est indiqué avec une liste de types de paramètres entre parenthèses et une flèche vers le type de retour. Considérons cette fonction :

funsafeDivide(numerator: Int, denominator: Int)=if(denominator ==0)0.0else numerator.toDouble()/ denominator

Elle prend deux Int et renvoie un Doubledonc son type est (Int, Int) -> Double. Nous pouvons faire référence à la fonction elle-même en faisant précéder son nom de la mention ::et nous pouvons l'affecter à une variable (dont le type serait normalement déduit, mais nous montrons la signature de type pour la démonstration) :

val f:(Int, Int)-> Double =::safeDivide

Lorsque vous avez une variable ou un paramètre de type fonction (parfois appelé une fonction référence de fonction), vous pouvez l'appeler comme s'il s'agissait d'une fonction ordinaire, et cela provoquera l'appel de la fonction référencée :

val quotient =f(3,0)

Il est possible pour une classe d'implémenter un type de fonction comme s'il s'agissait d'une interface. Elle doit alors fournir une fonction opérateur appelée invoke avec la signature donnée, et les instances de cette classe peuvent alors être affectées à une variable de ce type de fonction :

class Divider :(Int, Int)-> Double {overridefuninvoke(numerator: Int, denominator: Int): Double =...}

Les littéraux de fonction : expressions lambda et fonctions anonymes.

Comme en Python, on peut écrire des expressions lambda: des déclarations de fonctions sans nom avec une syntaxe très compacte, qui s'évaluent en objets de fonctions appelables. En Kotlin, les lambdas peuvent contenir plusieurs déclarations, ce qui les rend utiles pour des tâches plus complexes que les lambdas à expression unique de Python. La dernière instruction doit être une expression, dont le résultat deviendra la valeur de retour de la lambda (sauf si Unit soit le type de retour de la variable/paramètre à laquelle l'expression lambda est affectée, auquel cas le lambda n'a pas de valeur de retour). Une expression lambda est entourée d'accolades et commence par énumérer les noms de ses paramètres et éventuellement leurs types (sauf si les types peuvent être déduits du contexte) :

val safeDivide ={ numerator: Int, denominator: Int ->if(denominator ==0)0.0else numerator.toDouble()/ denominator
}

Le type de safeDivide est (Int, Int) -> Double. Notez que contrairement aux déclarations de type de fonction, la liste des paramètres d'une expression lambda ne doit pas être mise entre parenthèses.

Notez que les autres utilisations d'accolades dans Kotlin, comme dans les définitions de fonctions et de classes et après if/else/for/while ne sont pas des expressions lambda (il est donc pas le cas que if est une fonction qui exécute conditionnellement une fonction lambda).

Le type de retour d'une expression lambda est déduit du type de la dernière expression à l'intérieur (ou du type de fonction de la variable/paramètre à laquelle l'expression lambda est affectée). Si une expression lambda est passée en tant que paramètre de fonction (ce qui est l'utilisation ordinaire) ou assignée à une variable avec un type déclaré, Kotlin peut également déduire les types des paramètres, et vous devez seulement spécifier leurs noms :

val safeDivide:(Int, Int)-> Double ={ numerator, denominator ->if(denominator ==0)0.0else numerator.toDouble()/ denominator
}

Ou :

funcallAndPrint(function:(Int, Int)-> Double){println(function(2,0))}callAndPrint({ numerator, denominator ->if(denominator ==0)0.0else numerator.toDouble()/ denominator
})

Un lambda sans paramètre n'a pas besoin de la flèche. Un lambda à un paramètre peut choisir d'omettre le nom du paramètre et la flèche, dans ce cas le paramètre sera appelé. it:

val square:(Double)-> Double ={ it * it }

Si le type du dernier paramètre d'une fonction est un type de fonction et que vous souhaitez fournir une expression lambda, vous pouvez placer l'expression lambda à l'extérieur de des parenthèses du paramètre. Si l'expression lambda est le seul paramètre, vous pouvez omettre entièrement les parenthèses. Ceci est très utile pour la construction de DSLs.

funcallWithPi(function:(Double)-> Double){println(function(3.14))}

callWithPi { it * it }

Si vous voulez être plus explicite sur le fait que vous créez une fonction, vous pouvez faire une expression lambda. fonction anonyme, qui est toujours une expression plutôt qu'une déclaration :

callWithPi(fun(x: Double): Double {return x * x })

Ou :

callWithPi(fun(x: Double)= x * x)

Les expressions lambda et les fonctions anonymes sont collectivement appelées littéraux de fonction.

Compréhensions

Kotlin peut se rapprocher de la compacité de celle de Python. list/dict/set compréhensions. En supposant que people est une collection de Person avec un name propriété :

val shortGreetings = people
    .filter{ it.name.length <10}.map{"Hello, ${it.name}!"}

correspond à

short_greetings = [
    f"Hello, {p.name}"  # In Python 2, this would be: "Hello, %s!" % p.name
    for p in people
    if len(p.name) < 10
]

D'une certaine manière, c'est plus facile à lire car les opérations sont spécifiées dans l'ordre où elles sont appliquées aux valeurs. Le résultat sera un fichier immuable Listimmuable, où T est le type produit par les transformations que vous utilisez (dans ce cas, String). Si vous avez besoin d'une liste mutable, appelez toMutableList() à la fin. Si vous voulez un ensemble, appelez toSet() ou toMutableSet() à la fin. Si vous voulez transformer une collection en une carte, appelez associateBy(), qui prend deux lambdas qui spécifient comment extraire la clé et la valeur de chaque élément : people.associateBy({it.ssn}, {it.name}) (vous pouvez omettre la deuxième lambda si vous voulez que l'élément entier soit la valeur, et vous pouvez appeler toMutableMap() à la fin si vous voulez que le résultat soit mutable).

Ces transformations peuvent également être appliquées à Sequence, qui est similaire aux générateurs de Python et permet une évaluation paresseuse. Si vous avez une énorme liste et que vous voulez la traiter paresseusement, vous pouvez appeler. asSequence() sur elle.

Il y a une vaste collection d'opérations fonctionnelles de style programmation disponible dans le... kotlin.collections paquet.

Récepteurs

La signature d'une fonction membre ou d'une fonction d'extension commence par un élément récepteur: le type sur lequel la fonction peut être invoquée. Par exemple, la signature de toString() est Any.() -> String - elle peut être appelée sur n'importe quel objet non nul (le récepteur), elle ne prend aucun paramètre et elle renvoie un objet String. Il est possible d'écrire une fonction lambda avec une telle signature - cela s'appelle une fonction fonction littérale avec récepteur, et est extrêmement utile pour la construction de DSLs.

Un littéral de fonction avec récepteur est peut-être plus facile à penser comme une fonction d'extension sous la forme d'une expression lambda. La déclaration ressemble à une expression lambda ordinaire ; ce qui lui fait prendre un récepteur est le contexte - elle doit être passée à une fonction qui prend une fonction avec récepteur comme paramètre, ou assignée à une variable/propriété dont le type est un type de fonction avec récepteur. La seule façon d'utiliser une fonction avec récepteur est de l'invoquer sur une instance de la classe récepteur, comme s'il s'agissait d'une fonction membre ou d'une fonction d'extension. Par exemple :

classCar(val horsepowers: Int)val boast: Car.()-> String ={"I'm a car with $horsepowers HP!"}val car =Car(120)println(car.boast())

A l'intérieur d'une expression lambda avec récepteur, vous pouvez utiliser. this pour faire référence à l'objet récepteur (dans ce cas, car). Comme d'habitude, vous pouvez omettre this s'il n'y a pas de conflit d'appellation, c'est pourquoi nous pouvons simplement dire $horsepowers au lieu de ${this.horsepowers}. Méfiez-vous donc de cela en Kotlin, this peut avoir différentes significations selon le contexte : s'il est utilisé à l'intérieur d'expressions lambda (éventuellement imbriquées) avec des récepteurs, il fait référence à l'objet récepteur de l'expression lambda enfermante la plus intérieure avec récepteur. Si vous avez besoin de "sortir" du littéral de la fonction et d'obtenir l'objet "original". this (l'instance sur laquelle s'exécute la fonction membre dans laquelle vous vous trouvez), mentionnez le nom de la classe contenante après [email protected] - ainsi, si vous êtes à l'intérieur d'un littéral de fonction avec un récepteur à l'intérieur d'une fonction membre de Car, utilisez [email protected].

Comme pour les autres littéraux de fonction, si la fonction prend un paramètre (autre que l'objet récepteur sur lequel elle est invoquée), le paramètre unique est implicitement appelé ità moins que vous ne déclariez un autre nom. Si elle prend plus d'un paramètre, vous devez déclarer leurs noms.

Voici un petit exemple de DSL pour construire des structures arborescentes :

classTreeNode(val name: String){val children = mutableListOf<TreeNode>()funnode(name: String, initialize:(TreeNode.()-> Unit)?=null){val child =TreeNode(name)
        children.add(child)if(initialize !=null){
            child.initialize()}}}funtree(name: String, initialize:(TreeNode.()-> Unit)?=null): TreeNode {val root =TreeNode(name)if(initialize !=null){
        root.initialize()}return root
}val t =tree("root"){node("math"){node("algebra")node("trigonometry")}node("science"){node("physics")}}

Le bloc après tree("root") est le premier littéral de fonction avec récepteur, qui sera passé à tree() comme le initialize paramètre. Selon la liste des paramètres de tree()le récepteur est du type TreeNode, et par conséquent , tree() peut appeler initialize() sur root. root devient alors this à l'intérieur de la portée de cette expression lambda, donc lorsque nous appelons node("math")il dit implicitement this.node("math")this fait référence à la même TreeNode que root. Le bloc suivant est transmis à TreeNode.node()et est invoqué sur le premier enfant du bloc root à savoir mathet à l'intérieur de celui-ci, this fera référence à math.

Si nous avions voulu exprimer la même chose en Python, cela aurait ressemblé à ceci, et nous aurions été paralysés par le fait que les fonctions lambda ne peuvent contenir qu'une seule expression, donc nous avons besoin de définitions de fonctions explicites pour tout sauf les oneliners :

class TreeNode:
    def __init__(self, name):
        self.name = name
        self.children = []

    def node(self, name, initialize=None):
        child = TreeNode(name)
        self.children.append(child)
        if initialize:
            initialize(child)

def tree(name, initialize=None):
    root = TreeNode(name)
    if initialize:
        initialize(root)
    return root

def init_root(root):
    root.node("math", init_math)
    root.node("science",
              lambda science: science.node("physics"))

def init_math(math):
    math.node("algebra")
    math.node("trigonometry")

t = tree("root", init_root)

Les docs officiels ont également un exemple très cool avec un DSL pour construire des documents HTML.

Fonctions en ligne

Il y a un peu de surcharge d'exécution associée aux fonctions lambda : ce sont vraiment des objets, donc ils doivent être instanciés, et (comme les autres fonctions) les appeler prend un peu de temps aussi. Si nous utilisons la fonction inline sur une fonction, nous disons au compilateur de inline à la fois la fonction et ses paramètres lambda (s'il y en a) - c'est-à-dire que le compilateur copiera le code de la fonction (et ses paramètres lambda) dans le fichier chaque éliminant ainsi la surcharge de l'instanciation lambda et de l'appel de la fonction et des lambdas. Cela se produira de manière inconditionnelle, contrairement à ce qui se passe en C et C++, où la fonction inline est plutôt un indice pour le compilateur. Cela entraînera une augmentation de la taille du code compilé, mais cela peut en valoir la peine pour certaines fonctions petites mais fréquemment appelées.

inlinefuntime(action:()-> Unit): Long {val start = Instant.now().toEpochMilli()action()return Instant.now().toEpochMilli()- start
}

Maintenant, si vous faites :

val t = time {println("Lots of code")}println(t)

Le compilateur génèrera quelque chose comme ceci (sauf que start n'entrera pas en collision avec d'autres identifiants du même nom) :

val start = Instant.now().toEpochMilli()println("Lots of code")val t = Instant.now().toEpochMilli()- start
println(t)

Dans une définition de fonction en ligne, vous pouvez utiliser noinline devant tout paramètre typé fonction pour empêcher la lambda qui lui sera passée d'être également inline.

Fonctions utilitaires agréables

run(), let(), et with()

?. est agréable si vous voulez appeler une fonction sur quelque chose qui pourrait être nul. Mais que faire si vous voulez appeler une fonction qui prend un paramètre non nul, mais que la valeur que vous voulez passer pour ce paramètre pourrait être nulle ? Essayez run()qui est une fonction d'extension de Any? qui prend un lambda avec le récepteur comme paramètre et l'invoque sur la valeur sur laquelle il est appelé, et utiliser... ?. pour appeler run() seulement si l'objet n'est pas nul :

val result = maybeNull?.run{functionThatCanNotHandleNull(this)}

Si maybeNull est nul, la fonction ne sera pas appelée, et result sera nulle ; sinon, ce sera la valeur de retour de la fonction functionThatCanNotHandleNull(this), où this fait référence à maybeNull. Vous pouvez enchaîner run() avec des appels ?. - chacun sera appelé sur le résultat précédent s'il n'est pas nul :

val result = maybeNull
    ?.run{firstFunction(this)}?.run{secondFunction(this)}

Le premier this fait référence à maybeNullle second fait référence au résultat de firstFunction()et result sera le résultat de secondFunction() (ou nul si maybeNull ou l'un des résultats intermédiaires était nul).

Une variation syntaxique de run() est let(), qui prend un type de fonction ordinaire au lieu d'un type de fonction avec récepteur, de sorte que l'expression qui pourrait être nulle sera désignée par . it au lieu de this.

Les deux run() et let() sont également utiles si vous avez une expression que vous devez utiliser plusieurs fois, mais que vous ne vous souciez pas de lui trouver un nom de variable et de faire une vérification de nullité :

val result = someExpression?.let{firstFunction(it)
   it.memberFunction()+ it.memberProperty
}

Une autre version encore est with()que vous pouvez également utiliser pour éviter de trouver un nom de variable pour une expression, mais seulement si vous savez que son résultat sera non nul :

val result =with(someExpression){firstFunction(this)memberFunction()+ memberProperty
}

Dans la dernière ligne, il y a un implicite this. implicite devant les deux memberFunction() et memberProperty (si ceux-ci existent sur le type de someExpression). La valeur de retour est celle de la dernière expression.

apply() et also()

Si vous ne vous souciez pas de la valeur de retour de la fonction, mais que vous voulez faire un ou plusieurs appels impliquant quelque chose qui pourrait être nul et continuer à utiliser cette valeur, essayez. apply()qui renvoie la valeur sur laquelle elle est appelée. Ceci est particulièrement utile si vous voulez travailler avec de nombreux membres de l'objet en question :

maybeNull?.apply{firstFunction(this)secondFunction(this)
    memberPropertyA = memberPropertyB +memberFunctionA()}?.memberFunctionB()

À l'intérieur de l'objet apply bloc, this fait référence à maybeNull. Il y a un implicite this implicite devant memberPropertyA, memberPropertyBet memberFunctionA (à moins que ceux-ci n'existent pas sur maybeNull, auquel cas ils seront recherchés dans les scopes qui les contiennent). Ensuite , memberFunctionB() est également invoqué sur maybeNull.

Si vous trouvez le this est confuse, vous pouvez utiliser la syntaxe also à la place, qui prend des lambdas ordinaires :

maybeNull?.also{firstFunction(it)secondFunction(it)
    it.memberPropertyA = it.memberPropertyB + it.memberFunctionA()}?.memberFunctionB()

takeIf() et takeUnless()

Si vous voulez utiliser une valeur uniquement si elle satisfait à une certaine condition, essayez... takeIf(), qui retourne la valeur sur laquelle il est appelé si elle satisfait le prédicat donné, et null sinon. Il y a aussi takeUnless()qui inverse la logique. Vous pouvez suivre cela avec un ?. pour effectuer une opération sur la valeur uniquement si elle satisfait le prédicat. Ci-dessous, nous calculons le carré d'une certaine expression, mais seulement si la valeur de l'expression est au moins 42 :

val result = someExpression.takeIf{ it >=42}?.let{ it * it }

← Précédent : Sécurité des nuls Suivant : Paquets et importations →

Ce document a été rédigé par Aasmund Eldhuset; il est la propriété de Khan Academy et fait l'objet d'une licence d'utilisation sous CC BY-NC-SA 3.0 US. Veuillez noter que cela ne fait pas partie de l'offre officielle de produits de Khan Academy.