Jouer avec Swift, venant d'un arrière-plan Java, pourquoi voudriez-vous choisir un Struct au lieu d'une classe? On dirait que c'est la même chose, avec un Struct offrant moins de fonctionnalités. Pourquoi le choisir alors?
Jouer avec Swift, venant d'un arrière-plan Java, pourquoi voudriez-vous choisir un Struct au lieu d'une classe? On dirait que c'est la même chose, avec un Struct offrant moins de fonctionnalités. Pourquoi le choisir alors?
Réponses:
Selon la très populaire conférence WWDC 2015 sur la programmation orientée protocole dans Swift ( vidéo , transcription ), Swift fournit un certain nombre de fonctionnalités qui rendent les structures meilleures que les classes dans de nombreuses circonstances.
Les structures sont préférables si elles sont relativement petites et copiables car la copie est bien plus sûre que d'avoir plusieurs références à la même instance comme cela se produit avec les classes. Ceci est particulièrement important lors du passage d'une variable à de nombreuses classes et / ou dans un environnement multithread. Si vous pouvez toujours envoyer une copie de votre variable à d'autres endroits, vous n'avez jamais à vous soucier que cet autre endroit change la valeur de votre variable sous vous.
Avec Structs, il est beaucoup moins nécessaire de s'inquiéter des fuites de mémoire ou des courses de plusieurs threads pour accéder / modifier une seule instance d'une variable. (Pour les plus techniques, l'exception à cela est lors de la capture d'une structure à l'intérieur d'une fermeture, car elle capture en fait une référence à l'instance, sauf si vous la marquez explicitement pour la copie).
Les classes peuvent également devenir gonflées car une classe ne peut hériter que d'une seule superclasse. Cela nous encourage à créer d'énormes superclasses qui englobent de nombreuses capacités différentes qui ne sont que vaguement liées. L'utilisation de protocoles, en particulier avec des extensions de protocole où vous pouvez fournir des implémentations aux protocoles, vous permet d'éliminer le besoin de classes pour obtenir ce type de comportement.
L'exposé présente ces scénarios où les classes sont préférées:
- Copier ou comparer des instances n'a pas de sens (par exemple, Windows)
- La durée de vie de l'instance est liée aux effets externes (par exemple, TemporaryFile)
- Les instances ne sont que des "puits" - conduits en écriture seule vers l'état externe (par exempleCGContext)
Cela implique que les structures devraient être la valeur par défaut et que les classes devraient être une solution de rechange.
D'un autre côté, la documentation du langage de programmation Swift est quelque peu contradictoire:
Les instances de structure sont toujours transmises par valeur et les instances de classe sont toujours transmises par référence. Cela signifie qu'ils sont adaptés à différents types de tâches. Lorsque vous considérez les constructions de données et les fonctionnalités dont vous avez besoin pour un projet, décidez si chaque construction de données doit être définie en tant que classe ou structure.
En règle générale, envisagez de créer une structure lorsqu'une ou plusieurs de ces conditions s'appliquent:
- L'objectif principal de la structure est d'encapsuler quelques valeurs de données relativement simples.
- Il est raisonnable de s'attendre à ce que les valeurs encapsulées soient copiées plutôt que référencées lorsque vous affectez ou contournez une instance de cette structure.
- Toutes les propriétés stockées par la structure sont elles-mêmes des types de valeurs, qui devraient également être copiées plutôt que référencées.
- La structure n'a pas besoin d'hériter des propriétés ou du comportement d'un autre type existant.
Voici quelques exemples de bons candidats aux structures:
- Taille d'une forme géométrique, encapsulant peut-être une propriété width et une propriété height, toutes deux de type Double.
- Une façon de faire référence aux plages d'une série, peut-être en encapsulant une propriété de début et une propriété de longueur, toutes deux de type Int.
- Un point dans un système de coordonnées 3D, encapsulant peut-être des propriétés x, y et z, chacune de type Double.
Dans tous les autres cas, définissez une classe et créez des instances de cette classe à gérer et à transmettre par référence. En pratique, cela signifie que la plupart des constructions de données personnalisées doivent être des classes et non des structures.
Ici, il prétend que nous devrions utiliser par défaut les classes et utiliser les structures uniquement dans des circonstances spécifiques. En fin de compte, vous devez comprendre l'implication réelle des types de valeur par rapport aux types de référence, puis vous pouvez prendre une décision éclairée sur le moment d'utiliser des structures ou des classes. Gardez également à l'esprit que ces concepts sont en constante évolution et que la documentation du langage de programmation rapide a été écrite avant la présentation de la programmation orientée protocole.
In practice, this means that most custom data constructs should be classes, not structures.
Pouvez-vous m'expliquer comment, après avoir lu cela, vous obtenez que la plupart des ensembles de données devraient être des structures et non des classes? Ils ont donné un ensemble spécifique de règles quand quelque chose devrait être une structure et ont dit à peu près "tous les autres scénarios, une classe est meilleure".
Étant donné que les instances de structure sont allouées sur la pile et les instances de classe sont allouées sur le tas, les structures peuvent parfois être considérablement plus rapides.
Cependant, vous devez toujours le mesurer vous-même et décider en fonction de votre cas d'utilisation unique.
Prenons l'exemple suivant, qui illustre 2 stratégies d'encapsulation Int
de type de données à l'aide de struct
et class
. J'utilise 10 valeurs répétées pour mieux refléter le monde réel, où vous avez plusieurs champs.
class Int10Class {
let value1, value2, value3, value4, value5, value6, value7, value8, value9, value10: Int
init(_ val: Int) {
self.value1 = val
self.value2 = val
self.value3 = val
self.value4 = val
self.value5 = val
self.value6 = val
self.value7 = val
self.value8 = val
self.value9 = val
self.value10 = val
}
}
struct Int10Struct {
let value1, value2, value3, value4, value5, value6, value7, value8, value9, value10: Int
init(_ val: Int) {
self.value1 = val
self.value2 = val
self.value3 = val
self.value4 = val
self.value5 = val
self.value6 = val
self.value7 = val
self.value8 = val
self.value9 = val
self.value10 = val
}
}
func + (x: Int10Class, y: Int10Class) -> Int10Class {
return IntClass(x.value + y.value)
}
func + (x: Int10Struct, y: Int10Struct) -> Int10Struct {
return IntStruct(x.value + y.value)
}
La performance est mesurée en utilisant
// Measure Int10Class
measure("class (10 fields)") {
var x = Int10Class(0)
for _ in 1...10000000 {
x = x + Int10Class(1)
}
}
// Measure Int10Struct
measure("struct (10 fields)") {
var y = Int10Struct(0)
for _ in 1...10000000 {
y = y + Int10Struct(1)
}
}
func measure(name: String, @noescape block: () -> ()) {
let t0 = CACurrentMediaTime()
block()
let dt = CACurrentMediaTime() - t0
print("\(name) -> \(dt)")
}
Le code peut être trouvé à https://github.com/knguyen2708/StructVsClassPerformance
MISE À JOUR (27 mars 2018) :
Depuis Swift 4.0, Xcode 9.2, exécutant la version Release sur iPhone 6S, iOS 11.2.6, le paramètre du compilateur Swift est le suivant -O -whole-module-optimization
:
class
la version a pris 2,06 secondesstruct
la version a pris 4,17e-08 secondes (50 000 000 fois plus rapide)(Je ne fais plus la moyenne de plusieurs séries, car les écarts sont très faibles, moins de 5%)
Remarque : la différence est beaucoup moins dramatique sans optimisation du module entier. Je serais heureux si quelqu'un pouvait indiquer ce que fait réellement le drapeau.
MISE À JOUR (7 mai 2016) :
Depuis Swift 2.2.1, Xcode 7.3, exécutant la version Release sur iPhone 6S, iOS 9.3.1, en moyenne sur 5 exécutions, le paramètre du compilateur Swift est le suivant -O -whole-module-optimization
:
class
la version a pris 2.159942142sstruct
la version a pris 5,83E-08s (37 000 000 fois plus rapide)Remarque : comme quelqu'un a mentionné que dans les scénarios du monde réel, il y aurait probablement plus d'un champ dans une structure, j'ai ajouté des tests pour les structures / classes avec 10 champs au lieu de 1. Étonnamment, les résultats ne varient pas beaucoup.
RÉSULTATS ORIGINAUX (1er juin 2014):
(Exécuté sur struct / classe avec 1 champ, pas 10)
Depuis Swift 1.2, Xcode 6.3.2, exécutant la version Release sur iPhone 5S, iOS 8.3, en moyenne sur 5 exécutions
class
la version a pris 9.788332333sstruct
la version a pris 0.010532942s (900 fois plus rapide)ANCIENS RÉSULTATS (d'une heure inconnue)
(Exécuté sur struct / classe avec 1 champ, pas 10)
Avec la version construite sur mon MacBook Pro:
class
version a pris 1.10082 secstruct
version a pris 0.02324 sec (50 fois plus rapide)J'ai créé l'essentiel pour cela avec des exemples simples. https://github.com/objc-swift/swift-classes-vs-structures
les structures ne peuvent pas hériter rapidement. Si tu veux
class Vehicle{
}
class Car : Vehicle{
}
Allez en classe.
Les structures Swift passent par valeur et les instances de classe passent par référence.
Constante de structure et variables
Exemple (utilisé lors de la WWDC 2014)
struct Point{
var x = 0.0;
var y = 0.0;
}
Définit une structure appelée Point.
var point = Point(x:0.0,y:2.0)
Maintenant, si j'essaie de changer le x. C'est une expression valable.
point.x = 5
Mais si je définissais un point comme constant.
let point = Point(x:0.0,y:2.0)
point.x = 5 //This will give compile time error.
Dans ce cas, le point entier est immuable constant.
Si j'ai utilisé une classe Point à la place, c'est une expression valide. Parce que dans une classe, la constante immuable est la référence à la classe elle-même et non à ses variables d'instance (sauf si ces variables sont définies comme constantes)
Voici quelques autres raisons à considérer:
Les structures obtiennent un initialiseur automatique que vous n'avez pas du tout besoin de conserver dans le code.
struct MorphProperty {
var type : MorphPropertyValueType
var key : String
var value : AnyObject
enum MorphPropertyValueType {
case String, Int, Double
}
}
var m = MorphProperty(type: .Int, key: "what", value: "blah")
Pour obtenir ceci dans une classe, vous devez ajouter l'initialiseur et maintenir l'intialiseur ...
Les types de collection de base comme Array
les structures sont. Plus vous les utilisez dans votre propre code, plus vous vous habituerez à passer par valeur plutôt que par référence. Par exemple:
func removeLast(var array:[String]) {
array.removeLast()
println(array) // [one, two]
}
var someArray = ["one", "two", "three"]
removeLast(someArray)
println(someArray) // [one, two, three]
Apparemment, l'immuabilité contre la mutabilité est un sujet énorme, mais beaucoup de gens intelligents pensent que l'immuabilité - les structures dans ce cas - est préférable. Objets mutables vs objets immuables
internal
portée.
mutating
si vous êtes explicite sur les fonctions qui changent leur état. Mais leur nature en tant que types de valeur est ce qui est important. Si vous déclarez une structure avec let
vous ne pouvez pas appeler de fonctions de mutation dessus. La vidéo de la WWDC 15 sur Une meilleure programmation grâce aux types de valeur est une excellente ressource à ce sujet.
En supposant que nous savons que Struct est un type de valeur et Class est un type de référence .
Si vous ne savez pas ce qu'est un type de valeur et un type de référence, voir Quelle est la différence entre le passage par référence et le passage par valeur?
Basé sur le post de mikeash :
... Voyons d'abord quelques exemples extrêmes et évidents. Les entiers sont évidemment copiables. Il doit s'agir de types de valeur. Les sockets réseau ne peuvent pas être copiés de manière sensible. Ils doivent être des types de référence. Les points, comme dans les paires x, y, sont copiables. Il doit s'agir de types de valeur. Un contrôleur qui représente un disque ne peut pas être copié de manière sensible. Cela devrait être un type de référence.
Certains types peuvent être copiés, mais ce n'est peut-être pas quelque chose que vous souhaitez obtenir tout le temps. Cela suggère qu'ils devraient être des types de référence. Par exemple, un bouton à l'écran peut être conceptuellement copié. La copie ne sera pas tout à fait identique à l'original. Un clic sur la copie n'activera pas l'original. La copie n'occupera pas le même emplacement à l'écran. Si vous passez le bouton ou le placez dans une nouvelle variable, vous voudrez probablement vous référer au bouton d'origine, et vous ne voudrez faire une copie que lorsque cela est explicitement demandé. Cela signifie que votre type de bouton doit être un type de référence.
Les contrôleurs de vue et de fenêtre sont un exemple similaire. Ils peuvent être copiables, en théorie, mais ce n'est presque jamais ce que vous voudriez faire. Ils doivent être des types de référence.
Qu'en est-il des types de modèles? Vous pouvez avoir un type d'utilisateur représentant un utilisateur sur votre système ou un type de crime représentant une action entreprise par un utilisateur. Ceux-ci sont assez copiables, donc ils devraient probablement être des types de valeur. Cependant, vous voulez probablement que les mises à jour du crime d'un utilisateur effectuées à un endroit de votre programme soient visibles pour les autres parties du programme. Cela suggère que vos utilisateurs devraient être gérés par une sorte de contrôleur utilisateur qui serait un type de référence . par exemple
struct User {} class UserController { var users: [User] func add(user: User) { ... } func remove(userNamed: String) { ... } func ... }
Les collections sont un cas intéressant. Il s'agit notamment de tableaux et de dictionnaires, ainsi que de chaînes. Sont-ils copiables? Évidemment. La copie est-elle quelque chose que vous voulez arriver facilement et souvent? C'est moins clair.
La plupart des langues disent «non» à cela et font de leurs collections des types de référence. Cela est vrai dans Objective-C et Java et Python et JavaScript et dans presque tous les autres langages auxquels je peux penser. (Une exception majeure est C ++ avec les types de collection STL, mais C ++ est le fou fou du monde des langues qui fait tout étrangement.)
Swift a dit «oui», ce qui signifie que les types comme Array et Dictionary et String sont des structures plutôt que des classes. Ils sont copiés lors de l'affectation et lors de leur transmission en tant que paramètres. Il s'agit d'un choix tout à fait judicieux tant que la copie est bon marché, ce que Swift s'efforce de réaliser. ...
Personnellement, je ne nomme pas mes cours comme ça. Je nomme généralement le mien UserManager au lieu de UserController mais l'idée est la même
De plus, n'utilisez pas de classe lorsque vous devez remplacer chaque instance d'une fonction, c'est-à-dire qu'elles n'ont aucune fonctionnalité partagée .
Donc, au lieu d'avoir plusieurs sous-classes d'une classe. Utilisez plusieurs structures conformes à un protocole.
Un autre cas raisonnable pour les structures est lorsque vous voulez faire un delta / diff de votre ancien et nouveau modèle. Avec les types de références, vous ne pouvez pas le faire hors de la boîte. Avec les types de valeur, les mutations ne sont pas partagées.
Quelques avantages:
La structure est beaucoup plus rapide que la classe. De plus, si vous avez besoin d'héritage, vous devez utiliser Class. Le point le plus important est que la classe est le type de référence tandis que la structure est le type de valeur. par exemple,
class Flight {
var id:Int?
var description:String?
var destination:String?
var airlines:String?
init(){
id = 100
description = "first ever flight of Virgin Airlines"
destination = "london"
airlines = "Virgin Airlines"
}
}
struct Flight2 {
var id:Int
var description:String
var destination:String
var airlines:String
}
permet maintenant de créer une instance des deux.
var flightA = Flight()
var flightB = Flight2.init(id: 100, description:"first ever flight of Virgin Airlines", destination:"london" , airlines:"Virgin Airlines" )
laisse maintenant passer ces instances à deux fonctions qui modifient l'identifiant, la description, la destination etc.
func modifyFlight(flight:Flight) -> Void {
flight.id = 200
flight.description = "second flight of Virgin Airlines"
flight.destination = "new york"
flight.airlines = "Virgin Airlines"
}
aussi,
func modifyFlight2(flight2: Flight2) -> Void {
var passedFlight = flight2
passedFlight.id = 200
passedFlight.description = "second flight from virgin airlines"
}
donc,
modifyFlight(flight: flightA)
modifyFlight2(flight2: flightB)
maintenant, si nous imprimons l'identifiant et la description du flightA, nous obtenons
id = 200
description = "second flight of Virgin Airlines"
Ici, nous pouvons voir que l'id et la description de FlightA sont modifiés parce que le paramètre passé à la méthode de modification pointe en fait vers l'adresse mémoire de l'objet flightA (type de référence).
maintenant, si nous imprimons l'id et la description de l'instance FLightB que nous obtenons,
id = 100
description = "first ever flight of Virgin Airlines"
Ici, nous pouvons voir que l'instance FlightB n'est pas modifiée car dans la méthode modifyFlight2, l'instance réelle de Flight2 est passée plutôt que référence (type de valeur).
Here we can see that the FlightB instance is not changed
Structs
sont value type
et Classes
sontreference type
Utilisez un value
type lorsque:
Utilisez un reference
type lorsque:
De plus amples informations peuvent également être trouvées dans la documentation Apple
https://docs.swift.org/swift-book/LanguageGuide/ClassesAndStructures.html
Information additionnelle
Les types de valeurs rapides sont conservés dans la pile. Dans un processus, chaque thread a son propre espace de pile, donc aucun autre thread ne pourra accéder directement à votre type de valeur. Par conséquent, aucune condition de concurrence critique, verrous, interblocages ou toute complexité de synchronisation de thread associée.
Les types de valeurs n'ont pas besoin d'allocation de mémoire dynamique ou de comptage de références, deux opérations coûteuses. En même temps, les méthodes sur les types de valeurs sont distribuées statiquement. Celles-ci créent un énorme avantage en faveur des types de valeur en termes de performances.
Pour rappel, voici une liste de Swift
Types de valeur:
Types de référence:
Répondant à la question du point de vue des types de valeur par rapport aux types de référence, ce blog d'Apple semble très simple:
Utilisez un type de valeur [par exemple, struct, enum] lorsque:
- La comparaison des données d'instance avec == est logique
- Vous souhaitez que les copies aient un état indépendant
- Les données seront utilisées dans le code sur plusieurs threads
Utilisez un type de référence [par exemple, classe] lorsque:
- La comparaison de l'identité d'instance avec === est logique
- Vous souhaitez créer un état mutable partagé
Comme mentionné dans cet article, une classe sans propriétés inscriptibles se comportera de manière identique avec une structure, avec (j'ajouterai) une mise en garde: les structures sont les meilleures pour les modèles thread-safe - une exigence de plus en plus imminente dans l'architecture d'application moderne.
Avec les classes, vous obtenez l'héritage et êtes transmis par référence, les structures n'ont pas d'héritage et sont transmises par valeur.
Il y a de grandes sessions WWDC sur Swift, cette question spécifique est répondue en détail dans l'une d'entre elles. Assurez-vous de les regarder, car cela vous permettra de vous mettre à jour beaucoup plus rapidement que le guide des langues ou l'iBook.
Je ne dirais pas que les structures offrent moins de fonctionnalités.
Bien sûr, le moi est immuable sauf dans une fonction de mutation, mais c'est tout.
L'héritage fonctionne bien tant que vous vous en tenez à la bonne vieille idée que chaque classe doit être abstraite ou finale.
Implémentez des classes abstraites en tant que protocoles et des classes finales en tant que structures.
La bonne chose à propos des structures est que vous pouvez rendre vos champs mutables sans créer d'état mutable partagé car la copie à l'écriture s'en charge :)
C'est pourquoi les propriétés / champs dans l'exemple suivant sont tous mutables, ce que je ne ferais pas en Java ou en C # ou dans les classes swift .
Exemple de structure d'héritage avec un peu d'utilisation sale et simple en bas dans la fonction nommée "exemple":
protocol EventVisitor
{
func visit(event: TimeEvent)
func visit(event: StatusEvent)
}
protocol Event
{
var ts: Int64 { get set }
func accept(visitor: EventVisitor)
}
struct TimeEvent : Event
{
var ts: Int64
var time: Int64
func accept(visitor: EventVisitor)
{
visitor.visit(self)
}
}
protocol StatusEventVisitor
{
func visit(event: StatusLostStatusEvent)
func visit(event: StatusChangedStatusEvent)
}
protocol StatusEvent : Event
{
var deviceId: Int64 { get set }
func accept(visitor: StatusEventVisitor)
}
struct StatusLostStatusEvent : StatusEvent
{
var ts: Int64
var deviceId: Int64
var reason: String
func accept(visitor: EventVisitor)
{
visitor.visit(self)
}
func accept(visitor: StatusEventVisitor)
{
visitor.visit(self)
}
}
struct StatusChangedStatusEvent : StatusEvent
{
var ts: Int64
var deviceId: Int64
var newStatus: UInt32
var oldStatus: UInt32
func accept(visitor: EventVisitor)
{
visitor.visit(self)
}
func accept(visitor: StatusEventVisitor)
{
visitor.visit(self)
}
}
func readEvent(fd: Int) -> Event
{
return TimeEvent(ts: 123, time: 56789)
}
func example()
{
class Visitor : EventVisitor
{
var status: UInt32 = 3;
func visit(event: TimeEvent)
{
print("A time event: \(event)")
}
func visit(event: StatusEvent)
{
print("A status event: \(event)")
if let change = event as? StatusChangedStatusEvent
{
status = change.newStatus
}
}
}
let visitor = Visitor()
readEvent(1).accept(visitor)
print("status: \(visitor.status)")
}
Dans Swift, un nouveau modèle de programmation a été introduit connu sous le nom de programmation orientée protocole.
Modèle de création:
En rapide, Struct est une valeur types qui est automatiquement cloné. Par conséquent, nous obtenons le comportement requis pour implémenter gratuitement le modèle de prototype.
Alors que les classes sont le type de référence, qui n'est pas automatiquement cloné lors de l'affectation. Pour implémenter le modèle prototype, les classes doivent adopter le NSCopying
protocole.
La copie superficielle duplique uniquement la référence, qui pointe vers ces objets, tandis que la copie profonde duplique la référence de l'objet.
L'implémentation de la copie profonde pour chaque type de référence est devenue une tâche fastidieuse. Si les classes incluent un type de référence supplémentaire, nous devons implémenter un modèle de prototype pour chacune des propriétés de référence. Et puis nous devons réellement copier le graphique d'objet entier en implémentant le NSCopying
protocole.
class Contact{
var firstName:String
var lastName:String
var workAddress:Address // Reference type
}
class Address{
var street:String
...
}
En utilisant des structures et des énumérations , nous avons simplifié notre code car nous n'avons pas à implémenter la logique de copie.
De nombreuses API Cocoa nécessitent des sous-classes NSObject, ce qui vous oblige à utiliser la classe. Mais à part cela, vous pouvez utiliser les cas suivants du blog Swift d'Apple pour décider d'utiliser un type de valeur struct / enum ou un type de référence de classe.
Un point qui n'attire pas l'attention dans ces réponses est qu'une variable contenant une classe par rapport à une structure peut parfois let
permettre des modifications sur les propriétés de l'objet, alors que vous ne pouvez pas le faire avec une structure.
Ceci est utile si vous ne voulez pas que la variable pointe vers un autre objet, mais que vous devez quand même modifier l'objet, c'est-à-dire dans le cas de plusieurs variables d'instance que vous souhaitez mettre à jour l'une après l'autre. S'il s'agit d'une structure, vous devez permettre à la variable d'être réinitialisée sur un autre objet en utilisant complètement var
pour ce faire, car un type de valeur constante dans Swift permet correctement la mutation zéro, tandis que les types de référence (classes) ne se comportent pas de cette façon.
Comme les structures sont des types de valeur et que vous pouvez créer très facilement la mémoire qui est stockée dans la pile, la structure peut être facilement accessible et après la portée du travail, elle est facilement désallouée de la mémoire de la pile via pop depuis le haut de la pile. D'un autre côté, la classe est un type de référence qui stocke en tas et les modifications apportées dans un objet de classe auront un impact sur l'autre objet car elles sont étroitement couplées et de type de référence.Tous les membres d'une structure sont publics alors que tous les membres d'une classe sont privés .
L'inconvénient de struct est qu'il ne peut pas être hérité.
La structure et la classe sont des types de données définis par l'utilisateur
Par défaut, la structure est publique alors que la classe est privée
La classe implémente le principe de l'encapsulation
Les objets d'une classe sont créés sur la mémoire du tas
La classe est utilisée pour la réutilisabilité tandis que la structure est utilisée pour regrouper les données dans la même structure
Les membres de données de structure ne peuvent pas être initialisés directement mais ils peuvent être affectés par l'extérieur de la structure
Les membres de données de classe peuvent être initialisés directement par le constructeur sans paramètre et attribués par le constructeur paramétré