Le langage de programmation
Swift n'a que quatre ans, mais il devient déjà le principal langage de développement pour iOS. En développant la version 5.0, Swift est devenu un langage complexe et puissant qui répond à la fois à un paradigme orienté objet et fonctionnel. Et à chaque nouvelle version, il ajoute encore plus de fonctionnalités.
Mais comment connaissez-vous
vraiment Swift? Dans cet article, vous trouverez des exemples de questions pour une interview Swift.
Vous pouvez utiliser ces questions pour interroger les candidats afin de tester leurs connaissances ou vous pouvez tester les vôtres. Si vous ne connaissez pas la réponse, ne vous inquiétez pas: il y a une réponse à chaque question.
Les questions sont divisées en trois groupes:
- Débutant : pour les débutants. Vous avez lu quelques livres et appliqué Swift dans vos propres applications.
- Intermédiaire : adapté à ceux qui sont vraiment intéressés par la langue. Vous en avez déjà beaucoup lu et expérimentez souvent.
- Avancé : adapté aux développeurs les plus avancés - ceux qui aiment entrer dans la jungle de la syntaxe et utiliser des techniques avancées.
Il existe deux types de questions pour chaque niveau:
- écrit : adapté aux tests par e-mail, car ils suggèrent d'écrire du code.
- oral : peut être utilisé lorsque vous parlez au téléphone ou en personne, car il y a suffisamment de réponses en mots.
Pendant que vous lisez cet article, gardez le
terrain de jeu ouvert pour pouvoir vérifier le code de la question. Toutes les réponses ont été testées sur
Xcode 10.2 et
Swift 5 .
Débutant
questions écritesQuestion 1Considérez le code suivant:
struct Tutorial { var difficulty: Int = 1 } var tutorial1 = Tutorial() var tutorial2 = tutorial1 tutorial2.difficulty = 2
Quelles sont les valeurs de
tutorial1.difficulty et
tutorial2.difficulty ? Y aurait-il une différence si le
didacticiel était une classe? Pourquoi?
la réponsetutorial1.difficulty vaut 1 et tutorial2.difficulty vaut 2.
Dans Swift, les structures sont des types de valeur. Ils sont copiés, non référencés. La ligne suivante copie
tutorial1 et l'affecte Ă
tutorial2 :
var tutorial2 = tutorial1
Les modifications apportĂ©es Ă
tutorial2 n'affectent pas
tutorial1 .
Si Tutorial était une classe,
tutorial1.difficulty et
tutorial2.difficulty seraient égaux à 2. Les classes dans Swift sont des types de référence. Lorsque vous modifiez la propriété tutorial1, vous verrez la même modification pour tutorial2 - et vice versa.
Question 2Vous avez déclaré
view1 avec
var et
view2 avec
let . Quelle est la différence et la dernière ligne est-elle compilée?
import UIKit var view1 = UIView() view1.alpha = 0.5 let view2 = UIView() view2.alpha = 0.5
la réponseOui, la dernière ligne est compilée.
view1 est une variable et vous pouvez affecter sa valeur Ă une nouvelle instance de UIView. En utilisant
let , vous ne pouvez affecter une valeur qu'une seule fois, de sorte que le code suivant ne se compile pas:
view2 = view1
Cependant, UIView est une classe avec une sémantique référentielle, vous pouvez donc modifier les propriétés de view2 - ce qui signifie que le code sera
compilé .
Question 3Ce code trie le tableau par ordre alphabétique. Simplifiez autant que possible la fermeture.
var animals = ["fish", "cat", "chicken", "dog"] animals.sort { (one: String, two: String) -> Bool in return one < two } print(animals)
la réponseSwift
détermine automatiquement le type de paramètres de fermeture et le type de retour, vous pouvez donc les
supprimer :
animals.sort { (one, two) in return one < two }
Vous pouvez remplacer les noms de paramètres à l'aide de la notation
$ i :
animals.sort { return $0 < $1 }
Les fermetures consistant en une seule instruction peuvent ne pas contenir le mot-clé
return . La valeur de la dernière instruction exécutée devient le résultat de retour de la fermeture:
animals.sort { $0 < $1 }
Enfin, puisque Swift sait que les éléments du tableau sont conformes au protocole
Equatable , vous pouvez simplement écrire:
animals.sort(by: <)
Upd:
hummingbirddj simplifié encore plus:
Dans ce cas, vous pouvez mĂŞme raccourcir: animals.sort()
- Trie par ordre croissant, fonctionne pour les types qui implémentent Comparable.
Question 4Ce code crée deux classes:
Adresse et
Personne . Deux instances de la classe
Person (
Ray et
Brian ) sont également créées.
class Address { var fullAddress: String var city: String init(fullAddress: String, city: String) { self.fullAddress = fullAddress self.city = city } } class Person { var name: String var address: Address init(name: String, address: Address) { self.name = name self.address = address } } var headquarters = Address(fullAddress: "123 Tutorial Street", city: "Appletown") var ray = Person(name: "Ray", address: headquarters) var brian = Person(name: "Brian", address: headquarters)
Supposons que
Brian a déménagé vers une nouvelle adresse et que vous souhaitez mettre à jour son enregistrement comme suit:
brian.address.fullAddress = "148 Tutorial Street"
Cela se compile et s'exécute sans erreur. Mais, si vous vérifiez maintenant l'adresse de
Ray , vous verrez qu'il a également
«déménagé» .
Que s'est-il passé ici et comment pouvons-nous y remédier?
la réponseL'adresse est une classe et possède une sémantique de référence . Ainsi, le quartier général est la même instance de la classe partagée par ray et brian. Changer le siège changera l'adresse des deux.
Pour résoudre ce problème, vous pouvez créer une nouvelle instance de la classe Address et l'affecter à Brian, ou déclarer Address en tant que structure au lieu d'une classe .
questions oralesQuestion 1Qu'est-ce qui est
facultatif et quels problèmes résolvent-ils?
la réponsefacultatif permet à une variable de tout type de présenter une situation " sans valeur ". Dans Objective-C, "aucune valeur" n'était disponible que dans les types de référence utilisant la valeur spéciale nil . Les types de valeur, tels que int ou float , n'avaient pas cette capacité.
Swift a étendu le concept de «aucune valeur» aux types de valeur. La variable facultative peut contenir une valeur ou nil , indiquant l'absence d'une valeur.
Question 2Énumérez brièvement les principales différences entre la
structure et la
classe .
la réponseLes classes prennent en charge l'héritage, mais pas les structures.
Les classes sont un type de référence, les structures sont un type de valeur.
Question 3Que sont les
génériques et à quoi servent-ils?
la réponseDans Swift, vous pouvez utiliser des
génériques dans les classes, les structures et les énumérations.
Les génériques résolvent le problème de duplication de code. Si vous avez une méthode qui accepte les paramètres d'un type, vous devez parfois dupliquer du code pour travailler avec des paramètres d'un autre type.
Par exemple, dans ce code, la deuxième fonction est un «clone» de la première, sauf qu'elle a des paramètres de chaîne et non un entier.
func areIntEqual(_ x: Int, _ y: Int) -> Bool { return x == y } func areStringsEqual(_ x: String, _ y: String) -> Bool { return x == y } areStringsEqual("ray", "ray")
En utilisant des génériques, vous combinez deux fonctions en une et garantissez en même temps la sécurité du type:
func areTheyEqual<T: Equatable>(_ x: T, _ y: T) -> Bool { return x == y } areTheyEqual("ray", "ray") areTheyEqual(1, 1)
Puisque vous testez l'égalité, vous limitez les types à ceux qui sont conformes au protocole
Equatable . Ce code fournit le résultat souhaité et empêche le transfert de paramètres de type incorrect.
Question 4Dans certains cas, il ne sera pas possible d'éviter les options implicitement
déballées . Quand et pourquoi?
la réponseLes raisons les plus courantes d'utiliser des options implicitement non enveloppées sont:
- lorsque vous ne pouvez pas initialiser une propriété qui n'est pas nulle au moment de la création. Un exemple typique est le point de vente d'Interface Builder, qui est toujours initialisé après son propriétaire. Dans ce cas particulier, si tout est correctement configuré dans Interface Builder, vous avez la garantie que la prise n'est pas nulle avant de l'utiliser.
- pour résoudre le problème de la boucle des références fortes , lorsque deux instances de classes se réfèrent l'une à l'autre et qu'une référence non nulle à une autre instance est requise. Dans ce cas, vous marquez le lien d'un côté comme non propriétaire , et de l'autre côté utilisez une extension optionnelle implicite.
Question 5Quelles sont les méthodes de déploiement facultatives? Évaluez-les en termes de sécurité.
var x : String? = "Test"
Astuce: seulement 7 façons.
la réponseLe déballage forcé n'est pas sûr.
let a: String = x!
Le déploiement implicite lors de la déclaration d'une variable n'est pas sûr.
var a = x!
La reliure optionnelle est sûre.
if let a = x { print("x was successfully unwrapped and is = \(a)") }
Le chaînage optionnel est sûr.
let a = x?.count
L'opérateur de coalescence nul est sûr.
let a = x ?? ""
La déclaration de la
Garde est sûre.
guard let a = x else { return }
Modèle optionnel - sûr.
if case let a? = x { print(a) }
Intermédiaire
questions écritesQuestion 1Quelle est la difference entre
nil et
.none ?
la réponseIl n'y a aucune différence,
Optional.none (brièvement
.none ) et
nil sont équivalents.
En fait, l'instruction suivante retournera
vrai :
nil == .none
L'utilisation de
néant est plus généralement acceptée et recommandée.
Question 2Voici un modèle de thermomètre sous forme de classe et de structure. Le compilateur se plaint de la dernière ligne. Qu'est-ce qui ne va pas?
public class ThermometerClass { private(set) var temperature: Double = 0.0 public func registerTemperature(_ temperature: Double) { self.temperature = temperature } } let thermometerClass = ThermometerClass() thermometerClass.registerTemperature(56.0) public struct ThermometerStruct { private(set) var temperature: Double = 0.0 public mutating func registerTemperature(_ temperature: Double) { self.temperature = temperature } } let thermometerStruct = ThermometerStruct() thermometerStruct.registerTemperature(56.0)
la réponseThermometerStruct est correctement déclaré avec une fonction de mutation pour changer la variable interne. Le compilateur se plaint que vous appelez la méthode registerTemperature de l'instance qui a été créée avec let , donc cette instance est immuable. Changer let en var corrigera l'erreur de compilation.
Dans les structures, vous devez marquer les méthodes qui modifient les variables internes comme mutantes , mais vous ne pouvez pas appeler ces méthodes à l'aide d'une instance immuable.
Question 3Que produira ce code et pourquoi?
var thing = "cars" let closure = { [thing] in print("I love \(thing)") } thing = "airplanes" closure()
la réponseIl sera imprimé: j'adore les voitures. La liste de capture crée une copie de la variable lorsque la fermeture est déclarée. Cela signifie que la variable capturée ne changera pas sa valeur, même après l'attribution d'une nouvelle valeur.
Si vous omettez la liste de capture dans la fermeture, le compilateur utilisera le lien, pas la copie. L'appel de clôture reflétera le changement de la variable:
var thing = "cars" let closure = { print("I love \(thing)") } thing = "airplanes" closure()
Question 4Il s'agit d'une fonction qui compte le nombre de valeurs uniques dans un tableau:
func countUniques<T: Comparable>(_ array: Array<T>) -> Int { let sorted = array.sorted() let initial: (T?, Int) = (.none, 0) let reduced = sorted.reduce(initial) { ($1, $0.0 == $1 ? $0.1 : $0.1 + 1) } return reduced.1 }
Il utilise trié, il n'utilise donc que des types conformes au protocole comparable.
Vous pouvez l'appeler comme ceci:
countUniques([1, 2, 3, 3])
Réécrivez cette fonction en tant qu'extension de tableau afin de pouvoir l'utiliser comme ceci:
[1, 2, 3, 3].countUniques()
note du traducteurQuelque chose de trop douloureux est une fonction monstrueuse. Pourquoi pas:
func countUniques<T: Hashable>(_ array: Array<T>) -> Int { return Set(array).count }
la réponse extension Array where Element: Comparable { func countUniques() -> Int { let sortedValues = sorted() let initial: (Element?, Int) = (.none, 0) let reduced = sortedValues.reduce(initial) { ($1, $0.0 == $1 ? $0.1 : $0.1 + 1) } return reduced.1 } }
Question 5Voici une fonction qui divise deux doubles optionnels. Trois conditions doivent ĂŞtre remplies:
- le dividende ne doit pas ĂŞtre nul
- le diviseur ne doit pas ĂŞtre nul
- le diviseur ne doit pas ĂŞtre 0
func divide(_ dividend: Double?, by divisor: Double?) -> Double? { if dividend == nil { return nil } if divisor == nil { return nil } if divisor == 0 { return nil } return dividend! / divisor! }
Réécrivez cette fonction à l'aide de l'instruction
guard et non à l'aide du déballage forcé.
la réponseL'instruction
guard introduite dans Swift 2.0 fournit une sortie si la condition n'est pas remplie. Voici un exemple:
guard dividend != nil else { return nil }
Vous pouvez également utiliser l'instruction guard pour la
liaison facultative , après quoi la variable développée sera disponible en
dehors de l'instruction guard :
guard let dividend = dividend else { return .none }
Vous pouvez donc réécrire la fonction comme ceci:
func divide(_ dividend: Double?, by divisor: Double?) -> Double? { guard let dividend = dividend else { return nil } guard let divisor = divisor else { return nil } guard divisor != 0 else { return nil } return dividend / divisor }
Faites attention à l'absence de décompression forcée, car nous avons déjà décompressé le dividende et le diviseur et les avons placés dans des variables immuables non facultatives.
Notez également que le résultat des options non emballées dans la déclaration de garde est également disponible en dehors de la déclaration de garde.
Vous pouvez simplifier davantage la fonction en regroupant les instructions de garde:
func divide(_ dividend: Double?, by divisor: Double?) -> Double? { guard let dividend = dividend, let divisor = divisor, divisor != 0 else { return nil } return dividend / divisor }
Question 6Réécrivez la méthode de la question 5 à l'aide de l'
instruction if let .
la réponseL'instruction if let vous permet de décompresser les options et d'utiliser cette valeur dans ce bloc de code. En dehors de cela, ces valeurs ne seront pas disponibles.
func divide(_ dividend: Double?, by divisor: Double?) -> Double? { if let dividend = dividend, let divisor = divisor, divisor != 0 { return dividend / divisor } else { return nil } }
questions oralesQuestion 1Dans Objective-C, vous déclarez une constante comme celle-ci:
const int number = 0;
Et donc dans Swift:
let number = 0
Quelle est la différence?
la réponseDans
Objective-C, une constante est initialisée
au moment de la compilation avec une valeur qui devrait ĂŞtre connue Ă ce stade.
Une valeur immuable créée avec
let est une constante définie
au moment de l'exécution . Vous pouvez l'initialiser avec une expression statique ou dynamique. Par conséquent, nous pouvons le faire:
let higherNumber = number + 5
Veuillez noter qu'une telle mission ne peut être effectuée qu'une seule fois.
Question 2Pour déclarer une propriété ou une fonction statique pour les types de valeur, le modificateur
statique est utilisé. Voici un exemple pour la structure:
struct Sun { static func illuminate() {} }
Et pour les classes, il est possible d'utiliser
des modificateurs
statiques ou de
classe . Le résultat est le même, mais il y a une différence. Décrivez-le.
la réponsestatique rend une propriété ou une fonction statique et
sans chevauchement . L'utilisation de la classe
remplacera une propriété ou une fonction.
Ici, le compilateur jurera lors d'une tentative de remplacement de
illuminate () :
class Star { class func spin() {} static func illuminate() {} } class Sun : Star { override class func spin() { super.spin() }
Question 3Est-il possible d'ajouter une
propriété stockée à un type à l'aide d'une
extension ? Comment ou pourquoi pas?
la réponseNon, ce n'est pas possible. Nous pouvons utiliser l'extension pour ajouter un nouveau comportement à un type existant, mais nous ne pouvons pas changer le type lui-même ou son interface. Pour stocker la nouvelle propriété stockée, nous avons besoin de mémoire supplémentaire et l'extension ne peut pas le faire.
Question 4Qu'est-ce qu'un protocole dans Swift?
la réponseUn protocole est un type qui définit un aperçu des méthodes, des propriétés, etc. Une classe, une structure ou une énumération peut prendre un protocole pour implémenter tout cela. Le protocole lui-même n'implémente pas la fonctionnalité, mais la définit.
Avancé
questions écritesQuestion 1Supposons que nous ayons une structure qui définit le modèle d'un thermomètre:
public struct Thermometer { public var temperature: Double public init(temperature: Double) { self.temperature = temperature } }
Pour créer une instance, nous écrivons:
var t: Thermometer = Thermometer(temperature:56.8)
Mais quelque chose comme ça serait beaucoup plus pratique:
var thermometer: Thermometer = 56.8
Est-ce possible? Comment?
la réponseSwift définit des protocoles qui vous permettent d'initialiser un type à l'aide de littéraux par affectation. L'application du protocole approprié et la fourniture d'un initialiseur public permettront l'initialisation à l'aide de littéraux. Dans le cas du thermomètre, nous implémentons
ExpressibleByFloatLiteral :
extension Thermometer: ExpressibleByFloatLiteral { public init(floatLiteral value: FloatLiteralType) { self.init(temperature: value) } }
Maintenant, nous pouvons créer une instance comme celle-ci:
var thermometer: Thermometer = 56.8
Question 2Swift dispose d'un ensemble d'opérateurs prédéfinis pour les opérations arithmétiques et logiques. Il vous permet également de créer vos propres opérateurs, à la fois unaires et binaires.
Définissez et implémentez votre propre opérateur d'exponentiation (^^) selon les exigences suivantes:
- prend deux int comme paramètres
- renvoie le résultat de l'augmentation du premier paramètre à la puissance du second
- traite correctement l'ordre des opérations algébriques
- ignore les éventuelles erreurs de débordement
la réponseLa création d'un nouvel opérateur se déroule en deux étapes: l'annonce et la mise en œuvre.
La déclaration utilise le mot-clé
operator pour spécifier le type (unaire ou binaire), pour spécifier la séquence de caractères du nouvel opérateur, son associativité et la priorité d'exécution.
Ici, l'opérateur est ^^ et son type est infixe (binaire). L'associativité a raison.
Swift n'a pas d'ancienneté prédéfinie pour l'exponentiation. En algèbre, l'exponentiation doit être calculée avant multiplication / division. Ainsi, nous créons un ordre d'exécution personnalisé en plaçant l'exponentiation plus haut que la multiplication.
Cette annonce:
precedencegroup ExponentPrecedence { higherThan: MultiplicationPrecedence associativity: right } infix operator ^^: ExponentPrecedence
Voici l'implémentation:
func ^^(base: Int, exponent: Int) -> Int { let l = Double(base) let r = Double(exponent) let p = pow(l, r) return Int(p) }
Question 3Le code suivant définit la structure de
Pizza et le protocole
Pizzeria avec une extension pour l'implémentation par défaut de la méthode
makeMargherita () :
struct Pizza { let ingredients: [String] } protocol Pizzeria { func makePizza(_ ingredients: [String]) -> Pizza func makeMargherita() -> Pizza } extension Pizzeria { func makeMargherita() -> Pizza { return makePizza(["tomato", "mozzarella"]) } }
Nous définissons maintenant le restaurant
Lombardis :
struct Lombardis: Pizzeria { func makePizza(_ ingredients: [String]) -> Pizza { return Pizza(ingredients: ingredients) } func makeMargherita() -> Pizza { return makePizza(["tomato", "basil", "mozzarella"]) } }
Le code suivant crée deux instances de
Lombardis . Lequel d'entre eux fait de la margarita au basilic?
let lombardis1: Pizzeria = Lombardis() let lombardis2: Lombardis = Lombardis() lombardis1.makeMargherita() lombardis2.makeMargherita()
la réponseDans les deux. Le protocole
Pizzeria déclare la méthode
makeMargherita () et fournit une implémentation par défaut. L'implémentation de
Lombardis remplace la méthode par défaut. Puisque nous avons déclaré la méthode dans le protocole à deux endroits, l'implémentation correcte sera appelée.
Mais que se passe-t-il si le protocole ne déclare pas la méthode
makeMargherita () et que l'extension fournit toujours l'implémentation par défaut, comme ceci:
protocol Pizzeria { func makePizza(_ ingredients: [String]) -> Pizza } extension Pizzeria { func makeMargherita() -> Pizza { return makePizza(["tomato", "mozzarella"]) } }
Dans ce cas, seul lombardis2 aurait une pizza au basilic, tandis que lombardis1 n'aurait pas de pizza, car il utiliserait la méthode définie en extension.
Question 4Le code suivant ne se compile pas. Pouvez-vous expliquer ce qui ne va pas avec lui? Suggérez des solutions au problème.
struct Kitten { } func showKitten(kitten: Kitten?) { guard let k = kitten else { print("There is no kitten") } print(k) }
Astuce: Il existe trois façons de corriger l'erreur.
la réponseLe bloc
else de l'instruction
guard nécessite une option de sortie, soit en utilisant
return , en lançant une exception ou en appelant
@noreturn . La solution la plus simple consiste Ă ajouter une
déclaration de retour .
func showKitten(kitten: Kitten?) { guard let k = kitten else { print("There is no kitten") return } print(k) }
Cette solution lèvera une exception:
enum KittenError: Error { case NoKitten } struct Kitten { } func showKitten(kitten: Kitten?) throws { guard let k = kitten else { print("There is no kitten") throw KittenError.NoKitten } print(k) } try showKitten(kitten: nil)
Enfin, voici l'appel Ă
fatalError () , la fonction
@noreturn .
struct Kitten { } func showKitten(kitten: Kitten?) { guard let k = kitten else { print("There is no kitten") fatalError() } print(k) }
questions oralesQuestion 1Les fermetures sont-elles un type de référence ou un type de valeur?
la réponseLes fermetures sont un type de référence. Si vous affectez une fermeture à une variable, puis la copiez dans une autre variable, vous copiez le lien vers la même fermeture et sa liste de capture.
Question 2Vous utilisez le type
UInt pour stocker un entier non signé. Il implémente un initialiseur pour convertir à partir d'un tout avec un signe:
init(_ value: Int)
Toutefois, le code suivant ne compile pas si vous spécifiez une valeur négative:
let myNegative = UInt(-1)
Les entiers signés, par définition, ne peuvent pas être négatifs. Cependant, il est possible d'utiliser la représentation d'un nombre négatif en mémoire pour le traduire en non signé.
Comment puis-je convertir un entier négatif en UInt tout en conservant sa représentation en mémoire?
la réponseIl existe un initialiseur pour cela:
UInt(bitPattern: Int)
Et utilisez:
let myNegative = UInt(bitPattern: -1)
Question 3Décrire les références circulaires dans Swift? Comment peuvent-ils être réparés?
la réponseLes références circulaires se produisent lorsque deux instances contiennent une référence forte l'une à l'autre, ce qui conduit à une fuite de mémoire car aucune de ces instances ne peut être libérée. Une instance ne peut pas être libérée tant qu'il y a encore des références fortes, mais une instance contient l'autre.
Cela peut être résolu en remplaçant le lien d'un côté en spécifiant le mot clé faible ou sans propriétaire .
Question 4Swift vous permet de créer des énumérations récursives. Voici un exemple d'une telle énumération qui contient une variante de nœud avec deux types associatifs, T et List:
enum List<T> { case node(T, List<T>) }
Il y aura une erreur de compilation. Qu'avons-nous manqué?
la réponseNous avons oublié le mot-clé
indirect , qui permet des options d'énumération récursives similaires:
enum List<T> { indirect case node(T, List<T>) }