I. Focus sur les fonctions▲
Dans ce chapitre, nous allons explorer les fonctions un peu plus en détail que ce que nous avons vu dans l’introduction.
I-A. Les bases▲
I-A-1. Fonction sans valeur de retour ni paramètres▲
Voici notre première fonction :
fun
direBonjour()
{
println("Bonjour"
)
}
fun
main(args: Array<String
>)
{
direBonjour()
direBonjour()
}
Plusieurs remarques :
- on utilise le mot-clef fun et c’est le premier mot de la définition de fonction ;
- ici, il n’y a aucune indication sur la valeur de retour et pour cause, dans ce cas-ci nous n’en avons pas besoin.
I-A-2. Fonction sans valeur de retour, mais avec paramètre▲
Maintenant, personnalisons un peu l’invite d’accueil :
fun
direBonjour(nom: String
)
{
println("Bonjour
${nom}
!"
)
}
fun
main(args: Array<String
>)
{
direBonjour("Hubert"
)
direBonjour("Francis"
)
}
Alors :
- pour chaque paramètre de la fonction, il faut faire attention à préciser sous la forme nom: type ! Nous avons donc précisé « nom » avant le type « String », en pensant aux deux points (attention, il ne doit pas y avoir d’espace entre le nom de la variable et les deux points) ;
- pour le message à afficher, nous utilisons ici l’interpolation de chaîne dont nous avons déjà parlé. C’est ici une occasion idéale d’écrire un code concis et compréhensible grâce à cette fonctionnalité.
I-A-3. Fonction avec un paramètre ayant une valeur par défaut▲
Maintenant, pourquoi ne pas fournir une valeur par défaut à notre invite d’accueil ?
fun
direBonjour(nom: String
= "le monde"
) {
println("Bonjour
${nom}
!"
)
}
fun
main(args: Array<String
>)
{
direBonjour("Hubert"
)
direBonjour("Francis"
)
direBonjour()
}
En fait, on peut définir autant de paramètres avec valeur par défaut que souhaité (même si le nombre de paramètres d’une fonction doit toujours rester raisonnable) mais il faut veiller à toujours les placer en dernier dans la liste des paramètres.
I-A-4. Fonction avec une valeur de retour▲
Dans ce cas, il faut préciser le type de valeur de retour dans la signature de la fonction (c’est le type de retour associé aux éventuels paramètres de la fonction).
fun
additionner(a: Int
, b: Int
= 0
): Int
{
return
a + b
}
fun
multiplier(a: Int
, b: Int
= 1
): Int
{
return
a * b
}
fun
main(args: Array<String
>) {
println(multiplier(additionner(2
,3
), additionner(5
)))
}
Remarques :
- c’est après la liste des paramètres que l’on précise le type de retour. Et, comme précédemment, il faut absolument éviter un quelconque espace entre la parenthèse fermante et les deux points ;
- c’est avec le mot-clef return, obligatoire, que l’on précise la valeur de retour ;
- rien ne nous empêche de combiner avec des valeurs par défaut : ici, j’ai choisi les éléments neutres des deux opérations (ceux qui permettent d’obtenir exactement la valeur du premier paramètre).
Veuillez également remarquer que :
- pour une fonction sans valeur de retour, on peut très bien préciser le type Unit, équivalent de void en Java/C++ ;
- le paramètre de la fonction « main » correspond à un tableau de String : il s’agit donc de la valeur des paramètres d’exécution du programme.
I-A-5. La valeur de retour est une simple expression▲
Si la valeur de retour est une expression, alors on peut utiliser la syntaxe simplifiée :
fun
additionner(a: Int
, b: Int
= 0
) = a+b
Cela fonctionne donc avec les expressions « if » et « when » notamment.
I-A-6. Appel avec un ordre de paramètres différent▲
Rien ne nous empêche d’appeler une fonction avec un ordre de paramètres différent.
fun
additionner(a: Int
, b: Int
= 0
) = a + b
fun
soustraire(a: Int
, b: Int
= 0
) = a - b
fun
multiplier(a: Int
, b: Int
= 1
) = a * b
fun
main(args: Array<String
>) {
println(multiplier(additionner(2
), soustraire(b = 5
, a = 10
)))
}
Vous pouvez remarquer l’inversion des paramètres de l’appel à la fonction soustraire.
I-A-7. Fonction dans une fonction▲
En Kotlin, il est aussi possible de déclarer une fonction à l’intérieur d’une fonction.
fun
saluer() {
fun
obtenirNom() = "Philae"
println("Bonjour,
${obtenirNom()}
!"
)
}
fun
main(args: Array<String
>) {
saluer()
}
Et c’est notamment l’une des conséquences du fait qu’en Kotlin, les fonctions sont également des valeurs. Et cela devient particulièrement intéressant lorsqu’on les utilise en tant que fonctions d’ordre supérieur.
I-A-8. Fonctions variadiques▲
Il est également possible de définir des fonctions avec un nombre variable d’arguments :
fun
additionPersonnalisee(vararg
params: Double
): Double
{
var
total = 0.0
for
(value in
params) total += value
return
total
}
fun
main(args: Array<String
>) {
println(additionPersonnalisee(10.0
, 20.0
, 30.0
, 40.0
, 50.0
))
}
En fait, c’est grâce au mot-clef vararg que le paramètre params accepte un nombre indéterminé de valeurs. Ainsi params sera en quelque sorte un Array<Double>. La conséquence directe est qu’on ne peut utiliser qu’un seul paramètre variadique par fonction (mais il ne doit pas forcément être déclaré en dernier).
Remarquez aussi que passer un simple tableau en tant que paramètre ne fonctionnera pas, pour cela il faut utiliser une petite astuce :
fun
additionPersonnalisee(vararg
params: Double
): Double
{
var
total = 0.0
for
(value in
params) total += value
return
total
}
fun
main(args: Array<String
>) {
println(additionPersonnalisee(*arrayOf(10.0
, 20.0
, 30.0
, 40.0
, 50.0
).toDoubleArray()))
}
L’étoile ! Ajouter une simple étoile devant le tableau permet de l’interpréter en tant que liste d’arguments et non plus comme un unique argument.
Remarquez aussi que params étant en fait un DoubleArray et non un Array<Double> (deux types différents !), il a fallu effectuer une conversion préalable.
I-B. Fonctions d’ordre supérieur▲
Une fonction d’ordre supérieur effectue au moins l’une des deux tâches suivantes :
- elle prend au moins une fonction en tant que paramètre ;
- elle retourne une fonction.
Vous vous doutez bien que cela ajoute une flexibilité à nos possibilités de codage.
I-B-1. Les exemples simples de la librairie standard▲
Dans la librairie standard, de nombreuses méthodes associées aux collections sont écrites en tant que fonctions d’ordre supérieur, notamment en acceptant au moins une fonction en entrée.
I-B-1-a. La fonction map▲
Admettons que nous disposons des données des élèves d’une classe, et chaque donnée dispose notamment d’une propriété prénom. Nous souhaitons convertir ce tableau de données en un simple tableau de chaînes de caractères.
Une solution dite « naïve » serait codée comme suit :
var
resultat = mutableListOf<String
>()
for
(eleve in
listeEleves) {
resultats.add(eleve.prenom)
}
Mais il y a bien mieux, grâce à la méthode map, dont notamment notre Array d’élèves bénéficie :
fun
extrairePrenom(eleve: Eleve) = eleve.prenom
fun
main(args: Array<String
>)
{
// recuperation du tableau d’élèves volontairement omise
val
resultats = listeEleves.map(::extrairePrenom)
}
Ici, j’ai créé une fonction qui permet d’extraire le prénom d’un élève de son objet le représentant (en supposant ici que la classe Eleve dispose de la propriété prénom) et ensuite, je passe la référence de la fonction. Et cela grâce à la paire de signes deux-points précédant son nom et uniquement son nom.
En effet, la fonction map a besoin de savoir quelle fonction appliquer à chacun des éléments. Ici, nous passons une référence sur une fonction existante, mais lorsque nous parlerons de fonctions anonymes, nous verrons une autre possibilité qui s’avère utile dans bien des cas.
Remarquez également que la fonction map ne modifie pas la collection originale, mais en retourne une nouvelle.
Voici un autre exemple que vous pouvez cette fois tester directement dans le playground Kotlin :
fun
maConversion(entree: Int
): Int
= entree * 50
fun
main(args: Array<String
>) {
var
resultats = arrayOf(1
,2
,3
,4
,5
,6
)
println(resultats.map(::maConversion))
println(java.util.Arrays.toString(resultats))
}
Attention toutefois, les tableaux (array) en Kotlin sont basés sur les tableaux natifs de Java, lesquels n’ont pas de formatage de leur affichage par défaut. Au lieu de cela, pour les tableaux, la méthode toString() par défaut affiche l’identifiant mémoire de la variable. C’est pour cela que je fais appel à java.util.Arrays.toString afin d’afficher les résultats. La méthode map retourne une liste qui est basée sur la classe ArrayList de Java et donc dispose déjà d’un affichage par défaut.
I-B-1-b. La méthode filter▲
Une autre méthode d’ordre supérieur souvent présente dans beaucoup de types de collections : la fonction filter. Elle permet simplement de retourner une nouvelle collection avec des éléments filtrés selon une fonction que l’on passe en tant que critère de sélection.
fun
estPair(entree: Int
): Boolean = entree % 2
== 0
fun
main(args: Array<String
>) {
var
resultats = arrayOf(1
,2
,3
,4
,5
,6
)
println(resultats.filter(::estPair))
println(java.util.Arrays.toString(resultats))
}
Tout comme la méthode map, la méthode filter ne modifie pas la collection originale.
I-B-1-c. La méthode reduce▲
Cette méthode est un peu plus complexe, mais peut s’avérer très utile si elle est comprise.
En tant que premier exemple, nous allons réaliser le calcul de la somme des éléments d’un tableau de chiffres. Je donne le code, avant de le commenter.
fun
additionner(a: Int
, b: Int
): Int
= a + b
fun
main(args: Array<String
>) {
var
resultats = arrayOf(1
,2
,3
,4
,5
,6
)
println(resultats.reduce(::additionner))
}
En fait, c’est comme si la méthode reduce effectuait l’algorithme suivant
- Créé une variable numérique valant 0 ;
- Prends les deux premiers chiffres du tableau, dans l’ordre : 1 et 2 ;
- Passe-les à la fonction « additionner » que j’ai reçue et conserve le résultat ;
- Ajoute ce résultat à la valeur courante de la variable ;
- Prends la 1re valeur du tableau qui n’a pas encore été exploitée (lors du 1er passage, ce sera l’élément d’indice 2) ;
- Passe la valeur courante de la variable en 1er paramètre et la valeur que je viens de récupérer en 2e paramètre à la fonction « additionner » et conserve le résultat ;
- Recommence à l’étape 4 jusqu’à ce qu’il ne reste plus de valeur à exploiter ;
- Retourne la valeur de la variable.
On peut donc dire que le premier paramètre de la fonction passée à la méthode « reduce » correspond à la valeur accumulée lors de l’appel, et le deuxième paramètre à la valeur nouvellement extraite du tableau. Pour être plus clair, j’aurais aussi pu définir la fonction « additionner » comme suit :
fun
additionner(accumulateur: Int
, courant: Int
): Int
= accumulateur + courant
Ceci est un type d’utilisation parmi tant d’autres.
I-B-2. Codage d’une fonction d’ordre supérieur▲
Voyons cela à travers un exemple simple. Je souhaite par exemple effectuer un calcul entre deux nombres, mais je veux, dans ma fonction de calcul, laisser à l’utilisateur le soin de préciser le calcul à effectuer.
fun
add(a: Int
, b: Int
) = a + b
fun
sub(a: Int
, b: Int
) = a - b
fun
mul(a: Int
, b: Int
) = a * b
fun
div(a: Int
, b:Int
) = a / b
fun
afficherCalcul(a: Int
, b: Int
, calcul: (a: Int
, b: Int
) -> Int
)
{
println("Le calcul de
${a}
et
${b}
vaut
${calcul(a,b)}
"
)
}
fun
main(args: Array<String
>) {
afficherCalcul(10
,5
, ::add)
afficherCalcul(10
,5
, ::sub)
afficherCalcul(10
,5
, ::mul)
afficherCalcul(10
,5
, ::div)
}
Ici, les quatre fonctions d’opération add, sub, mul et div ont la même signature (pour rappel, le nom des variables n’influe pas sur la définition d’une signature : manger(aliment : String) a exactement la même signature que manger(dessert : String)). Elles acceptent deux Int et retournent un Int (type déduit par le compilateur d’après chaque expression de retour).
On peut donc passer cette signature à notre fonction de calcul, de sorte que les quatre fonctions sont interchangeables. La signature est on ne peut plus simple à définir :
- la liste des paramètres ;
- le type de retour, mais précédé d’une flèche et non de deux-points.
I-C. Fonctions anonymes et lambda▲
Parfois, il peut s’avérer fastidieux de devoir créer une fonction, juste pour ne l’utiliser que dans un seul appel à une fonction/méthode d’ordre supérieur. Pour résoudre cette problématique, on peut utiliser les fonctions anonymes ou les lambda.
I-C-1. Fonction anonyme▲
Une fonction anonyme est une fonction qui, comme son nom l’indique, ne se voit pas attribuer d’identité, de nom. Il va de soi qu’une telle fonction ne peut pas être utilisée de manière classique :
- nous pouvons la stocker dans une variable (val ou var) ;
- ou la passer directement à une fonction/méthode d’ordre supérieur.
L’exemple de la fonction de calcul développée plus haut pourrait aussi être adapté comme suit :
fun
afficherCalcul(a: Int
, b: Int
, calcul: (a: Int
, b: Int
) -> Int
)
{
println("Le calcul de
${a}
et
${b}
vaut
${calcul(a,b)}
"
)
}
fun
main(args: Array<String
>) {
afficherCalcul(10
,5
, fun
(a: Int
, b: Int
) = a + b)
afficherCalcul(10
,5
, fun
(a: Int
, b: Int
) = a - b)
afficherCalcul(10
,5
, fun
(a: Int
, b: Int
) = a * b)
afficherCalcul(10
,5
, fun
(a: Int
, b:Int
) = a / b)
}
Rien de compliqué dans la définition d’une fonction anonyme.
I-C-2. Lambda▲
Les lambda, en plus de présenter une syntaxe différente des fonctions anonymes, présentent d’autres particularités.
I-C-2-a. Utilisation simple▲
D’abord, voyons une manière de réutiliser l’exemple précédent avec des lambda.
fun
afficherCalcul(a: Int
, b: Int
, calcul: (a: Int
, b: Int
) -> Int
)
{
println("Le calcul de
${a}
et
${b}
vaut
${calcul(a,b)}
"
)
}
fun
main(args: Array<String
>) {
afficherCalcul(10
,5
, {a: Int
, b: Int
-> a + b})
afficherCalcul(10
,5
, {a: Int
, b: Int
-> a - b})
afficherCalcul(10
,5
, {a: Int
, b: Int
-> a * b})
afficherCalcul(10
,5
, {a: Int
, b: Int
-> a / b})
}
Une lambda s’écrit simplement : { [paramètres] -> [expression/ liste d’instructions sans accolades] }
Si l’on souhaite réaliser plusieurs instructions dans la lambda, on les enchaîne simplement, sans mettre d’accolades.
fun
afficherCalcul(a: Int
, b: Int
, calcul: (a: Int
, b: Int
) -> Int
)
{
println("Le calcul de
${a}
et
${b}
vaut
${calcul(a,b)}
"
)
}
fun
main(args: Array<String
>) {
afficherCalcul(10
,5
, {a: Int
, b: Int
->
println("Dans le lambda de calcul"
)
a + b
})
}
Si la lambda doit retourner une valeur, elle correspondra alors à la valeur de la dernière instruction, qui doit donc, dans ce cas précis, être aussi une expression. (C’est le cas dans le code précédent.)
Tout comme les fonctions anonymes, les lambda peuvent être stockées dans des variables.
En revanche, on ne peut pas préciser le type de retour à une lambda : pour cela, il faudra utiliser une fonction anonyme à la place. En général, le compilateur parvient lui-même à vérifier si le type de retour de la lambda correspond bien au type attendu.
I-C-2-b. Lambda en tant que dernier paramètre d’une fonction▲
Une caractéristique sympathique des lambda est que si une lambda est le dernier paramètre d’une fonction/méthode, alors on peut « l’extraire des paramètres » et la placer juste derrière la fonction.
fun
afficherCalcul(a: Int
, b: Int
, calcul: (a: Int
, b: Int
) -> Int
)
{
println("Le calcul de
${a}
et
${b}
vaut
${calcul(a,b)}
"
)
}
fun
main(args: Array<String
>) {
afficherCalcul(10
,5
) {a: Int
, b: Int
-> a + b}
afficherCalcul(10
,5
) {a: Int
, b: Int
-> a - b}
afficherCalcul(10
,5
) {a: Int
, b: Int
-> a * b}
afficherCalcul(10
,5
) {a: Int
, b: Int
-> a / b}
}
Ici, le paramètre « calcul », qui définit une fonction à passer en paramètre, est le dernier paramètre de la fonction. Cela implique que si c’est une lambda que je passe, je peux la sortir de la liste des paramètres et la coller juste après l’appel de la fonction. C’est une simplification syntaxique qui peut clarifier les choses, même si dans le cas présent ça ne saute pas aux yeux.
On peut en tous cas séparer la liste des valeurs de la logique à y appliquer.
I-C-2-c. Lambda à paramètre unique▲
Vous souvenez-vous de l’exemple où l’on souhaitait trier un tableau en ne retenant que les nombres pairs ?
On pourrait commencer par le réécrire simplement avec une lambda :
fun
main(args: Array<String
>) {
var
resultats = arrayOf(1
,2
,3
,4
,5
,6
)
println(resultats.filter({a -> a % 2
== 0
}))
println(java.util.Arrays.toString(resultats))
}
Par contre, je trouve bien dommage de devoir créer un paramètre « a » juste pour tester ensuite son modulo. Pas vous ?
Et bien, il est possible de contourner cette bizarrerie :
fun
main(args: Array<String
>) {
var
resultats = arrayOf(1
,2
,3
,4
,5
,6
)
println(resultats.filter({it
% 2
== 0
}))
println(java.util.Arrays.toString(resultats))
}
Dans une lambda à unique argument, il est possible de faire appel à cet argument avec le mot-clef it. Une fois de plus, l’écriture du calcul est plus élégante.
II. Les classes : syntaxe de base▲
II-A. Classe vide▲
En kotlin, il est possible de déclarer et instancier des classes vides
class
ClasseVide
fun
main(args: Array<String
>) {
val
instance = ClasseVide()
}
Remarquez :
- les accolades ne sont pas obligatoires lors de la déclaration d’une classe vide ;
- on crée une classe simplement en suffixant le nom de la classe avec des parenthèses : aucun mot-clef new.
Évidemment, dans le cas présent, une telle classe vide ne sert à rien.
II-B. Classe englobant des propriétés et méthodes▲
Dès lors, nous pouvons ajouter des variables (une variable dans une classe est une propriété) et des fonctions (une fonction dans une classe est plus communément appelée méthode).
class
Personne {
val
nom = "Jean"
var
age = 0
fun
grandir() {
age += 1
}
}
fun
main(args: Array<String
>) {
val
humain = Personne()
println(humain.nom)
humain.age += 1
println(humain.age)
humain.grandir()
println(humain.age)
}
II-C. Visibilité des propriétés et méthodes dans les classes▲
Dans l’exemple précédent, nous pouvions accéder à la variable « age » de l’être humain directement depuis la fonction principale. En effet, en Kotlin, par défaut, les éléments d’une classe sont public. C’est-à-dire qu’on peut y accéder aussi bien dans le code de définition de la classe qu’à l’extérieur (ici, dans la fonction principale).
Mais il est possible de restreindre tout ou partie des variables et méthodes de la classe à une utilisation interne :
class
Personne {
val
nom = "Jean"
private
var
age = 0
fun
grandir() {
age += 1
}
fun
getAge() = age
}
fun
main(args: Array<String
>) {
val
humain = Personne()
println(humain.nom)
// Interdit !!! humain.age += 1
// Interdit !! println(humain.age)
println(humain.getAge())
humain.grandir()
println(humain.getAge())
}
Ici, nous avons restreint l’accès de la variable « age » à la classe, avec le mot-clef private. Cela permet notamment d’avoir un meilleur contrôle sur l’évolution de cette variable. Et pour permettre aux utilisateurs de continuer à pouvoir lire l’âge, une méthode supplémentaire getAge() a été ajoutée.
II-D. Personnalisation des propriétés mutables▲
Dans le code précédent, nous avons rendu la propriété « age » privée et créé une méthode getAge() afin de pouvoir y accéder depuis l’extérieur.
Mais nous pouvons faire mieux : laisser la propriété « age » publique et personnaliser son accesseur en écriture (setter).
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
class
Personne {
val
nom = "Jean"
var
age = 0
private
set
fun
grandir() {
age += 1
}
}
fun
main(args: Array<String
>) {
val
humain = Personne()
println(humain.nom)
// Interdit !!! humain.age += 1
println(humain.age)
humain.grandir()
println(humain.age)
}
Ainsi :
- dans la classe « Personne », nous avons restreint l’accès du setter de la propriété « age » à la classe elle-même ;
- dans la méthode principale, nous pouvons simplement accéder à la propriété « age », mais uniquement en lecture (ligne 11) : il est impossible de la modifier par un accès direct depuis la fonction principale (lignes 13 et 15).
Cette personnalisation de getter/setter est ici possible parce que la propriété « age » est variable (de type var).
III. Conclusion et remerciements▲
Dans cette section, nous avons vu les déclarations et des utilisations de fonctions, ainsi que les notions de base sur les classes.
Dans la section suivante, la dernière, nous verrons des concepts avancés sur les classes ainsi que diverses fonctionnalités utiles.
Je tiens à remercier escartefigue pour la relecture orthographique, et Mickael Baron pour la relecture technique.