Accès aux membres d'une struct par nom et par indice

CéroceCéroce Membre, Modérateur
février 2017 modifié dans Objective-C, Swift, C, C++ #1
Bonjour à  tous,

J'ai une question pour ceux qui maà®trisent Swift mieux que moi.
J'ai un vecteur à  trois composantes, x, y et z, et j'aimerais y accéder soit par le nom des composantes (vecteur.y), soit par un indice (vecteur[1]).

J'ai cette manière de faire, en auriez-vous une meilleure à  me proposer ? En l'état, ce qui me gène est la duplication de la donnée. Mais ce qui me plaà®t est que l'accès est aussi rapide dans les deux cas.
 
struct Vec3 {
let x: Float
let y: Float
let z: Float
let components: [Float]

init(_ x: Float, _ y:Float, _ z:Float) {
self.x = x
self.y = y
self.z = z
self.components = [x, y, z]
}
}
Mots clés:
«1

Réponses

  • Tu rajoutes le subscript : 



    struct Vec3 {
    let x: Float
    let y: Float
    let z: Float

    subscript(index: Int) -> Float {
    get {
    switch index {
    case 0 : return self.x
    case 1 : return self.y
    case 2 : return self.z
    default: assertionFailure("Valid indexes are [0, 1, 2]. \(index) is out of bounds")
    }
    return Float()
    }
    }
    }

    let v = Vec3(x: 1, y: 2, z: 3)
    v[0] // returns 1
    v[1] // returns 2
    v[2] // returns 3
    v[4] // fatal error: Valid indexes are [0, 1, 2]. 4 is out of bounds

    Tout simplement 


  • DrakenDraken Membre
    février 2017 modifié #3

    Moi j'ai ça :



    struct Vec3 {
    let components: [Float]

    var x:Float {
    get { return self.components[0] }
    }

    var y:Float {
    get { return self.components[1] }
    }

    var z:Float {
    get { return self.components[2] }
    }

    init(_ x: Float, _ y: Float, _ z:Float) {
    self.components = [x, y, z]
    }
    }



    Utilisation :



    let v = Vec3(1.0, 2.0, -1.0)
    let x1 = v.x
    print ("x1 : ", x1)
    let x2 = v.components[0]
    print ("x2 : ", x2)



     


    x1 :  1.0


    x2 :  1.0


     

  • CéroceCéroce Membre, Modérateur
    Merci tous les deux pour vos réponses.
    J'avoue avoir une préférence pour la version de Pyroh, la notation en subscript (vec[0]) étant moins lourde.


  • J'avoue avoir une préférence pour la version de Pyroh, la notation en subscript (vec[0]) étant moins lourde.




    Oui, j'ai pensé la même chose en lisant sa solution. Son approche est plus esthétique que la mienne. 

  • Y'a juste un truc qui me gène dans ma solution c'est le :



    return Float()

    aÌ€ la fin du subscript. Il serait bon soit d'avoir une solution un peu plus propre ou une possibilité dans Swift de pouvoir marquer une fonction comme étend faillible sans forcément utiliser les erreur avec un throws qui impliquerai plus de code chiant à  l'usage. 


     


    En fait j'aimerai savoir si quelqu'un à  une solution avant de proposer quelque chose dans Swift Evolution.


     


    Je n'ai encore eu le temps de regarder si c'était dedans.


  • CéroceCéroce Membre, Modérateur

    Y'a juste un truc qui me gène dans ma solution c'est le :


    return Float()
    aÌ€ la fin du subscript. Il serait bon soit d'avoir une solution un peu plus propre ou une possibilité dans Swift de pouvoir marquer une fonction comme étend faillible sans forcément utiliser les erreur avec un throws qui impliquerai plus de code chiant à  l'usage.


    ça ne me pose pas de problème, sachant que c'est un cas qui n'est pas sensé arriver sauf si je fais une erreur.

    À noter que je n'ai pas eu à  écrire get { } dans mon implémentation.
  • JérémyJérémy Membre
    février 2017 modifié #8

    Je suis bien consicient que la demande imposait de pouvoir accéder aux composants via un index de 0 à  2.





    struct Vec3 {
    let x: Float
    let y: Float
    let z: Float
    let components: [Float]

    init(_ x: Float, _ y:Float, _ z:Float) {
    self.x = x
    self.y = y
    self.z = z
    self.components = [x, y, z]
    }
    }



    Dans le cas de Pyroh on ne peut pas faire :



    let v = Vec3(1,2,3)
    v.components

    Si il me fallait absolument cette propriété pour X raison. Le mieux serait quoi ? De passer par une propriété calculée ?



    var components: [Float] {
    return [x,y,z]
    }

  • Voilà  une solution qui conviendrait normalement à  tout le monde :



    struct Vec3 {
    let x: Float
    let y: Float
    let z: Float
    var components: [Float] {
    get { return [x, y, z] }
    }

    subscript(index: Int) -> Float {
    return components[index]
    }
    }

    let v = Vec3(x: 1, y: 2, z: 3)
    v[0] // returns 1
    v[1] // returns 2
    v[2] // returns 3
    v[4] // fatal error: Index out of range

    components est certes calculée mais c'est pas super grave vu qu'on a que 3 occurrences.


    Après c'est surtout moi qui ne veut pas mettre d'init quand c'est pas vraiment nécessaire.


  • JérémyJérémy Membre
    février 2017 modifié #10


    components est certes calculée mais c'est pas super grave vu qu'on a que 3 occurrences.


    Après c'est surtout moi qui ne veut pas mettre d'init quand c'est pas vraiment nécessaire.




     


    Petite question au passage. Dans ce cas là , components serait calculé à  l'instanciation de l'objet ? Si oui (en admettant que Vec3 soit une classe) que se passerait il pour ce champ si je fais :



    let v = Vec3(1.0, 2.0, -1.0)
    v.x = 2.3
    v.y = v.x
    v.z = v.x * 2.0
    let c = v.components

    components est recalculé lorsque l'un des trois champs change de valeur ?


  • Je dirais que components est calculé à  chaque appel.


  • PyrohPyroh Membre
    février 2017 modifié #12

    Je dirai surtout qu'avec des let partout le compilo s'allumerai comme un sapin de Noël... 


    Sinon oui components est calculé à  chaque appel.


  • DrakenDraken Membre
    février 2017 modifié #13


    Je dirais que components est calculé à  chaque appel.




    Je suis du même avis. D'ailleurs c'est facile à  vérifier, avec un point d'arrêt ou un print().


     


    EDIT : Preuve par l'expérience


     


    La nouvelle version Vec3, avec l'init, et un message à  chaque régénération du tableau :



    struct Vec3 {
    let x: Float
    let y: Float
    let z: Float
    var components: [Float] {
    get {
    print ("recalcul du tableau [x,y,z]")
    return [x, y, z] }
    }

    subscript(index: Int) -> Float {
    return components[index]
    }

    init(_ x: Float, _ y: Float, _ z:Float) {
    self.x = x
    self.y = y
    self.z = z
    }
    }


    Mise en oeuvre :



    let v = Vec3(1.0, 2.0, -1.0)

    let x1 = v.x
    print ("x1 : ", x1)

    let x2 = v[0]
    print ("x2 : ", x2)

    let y2 = v[1]
    print ("y2 : ", y2)

    let z2 = v[2]
    print ("z2 : ", z2)


    Résultat :


     



     


    x1 :  1.0


    recalcul du tableau [x,y,z]


    x2 :  1.0


    recalcul du tableau [x,y,z]


    y2 :  2.0


    recalcul du tableau [x,y,z]


    z2 :  -1.0



     

    Je doute que Ceroce soit heureux de la perte de ressources (processeur et mémoire) occasionné par la reconstruction du tableau, à  chaque lecture d'une seule valeur. Ce n'est pas gênant pour un appel de temps en temps, mais lui bosse sur des moteurs de rendu 3D en ray-tracing. Il a besoin d'accéder des MILLIONS de fois par seconde à  des coordonnées x,y,z. Un micro-ralentissement répété des MILLIONS de fois à  la seconde, c'est .. problématique !
  • JérémyJérémy Membre
    février 2017 modifié #14


    Je dirai surtout qu'avec des let partout le compilo s'allumerai comme un sapin de Noël... 




     


    Bien vu ! o:)


    Nous supposerons que nous avons des var... ;)

     




    Je dirais que components est calculé à  chaque appel.




     


    J'aurais dit la même chose dans la mesure où le mot clé lazy (lazy var x: Int = {...}) n'aurait pas grand intérêt... :/


  • @Jérémy : qu'est-ce que ça change de mettre lazy ? C'est pour des let ?


  • JérémyJérémy Membre
    février 2017 modifié #16

    Non non non



    var x: Int {...}
    lazy var y: Int {...}

    x sera calculé lorsque l'objet sera instancié alors que y sera calculé lors de son premier appel.


     


    Exemple (le code est complètement débile mais c'est pour illustrer ce que je viens de dire) :



    class Point {
    var x: Int
    var y: Int

    var adding: Int = {
    return x + y
    }

    lazy var multiplaying: Int = {
    return x*y
    }

    init(_ x: Int, _ y: Int) {
    self.x = x
    self.y = y
    }
    }

    let point: Point = Point(3,2)
    // Ici adding aura été calculé alors que multiplaying sera à  nil
    // tant que tu ne l'auras pas appelé une première fois

  • Joanna CarterJoanna Carter Membre, Modérateur

    Jérémy. Je ne sais pas ce que tu crois que tu fasses  ::)


     


    On ne ni add, ni multiply, les coordonnées d'une pointe. En plus, là , il vaut mieux d'utiliser une struct qu'une classe


     


    À part du fait qu'il y a déjà  les structs Point, Size, etc en Swift, voici un exemple de comment on peut faire telle struct :



    struct Point
    {
    var x: Int

    var y: Int
    }

    func +(lhs: Point, rhs: Point) -> Point
    {
    return Point(x: lhs.x + rhs.x, y: lhs.y + rhs.y)
    }

    func *(lhs: Point, rhs: Int) -> Point
    {
    return Point(x: lhs.x * rhs, y: lhs.y * rhs)
    }

    // test
    {
    let p1 = Point(x: 5, y: 5)

    print(p1)

    let p2 = Point(x: 2, y: 3)

    print(p2)

    let p3 = p1 + p2

    print(p3)

    let p4 = p3 * 4

    print(p4)
    }
  • Joanna CarterJoanna Carter Membre, Modérateur
    février 2017 modifié #18

    Ou, pour jouer plus avec le vecteur de Céroce :



    struct Vector
    {
    var x: Float

    var y: Float

    var z: Float

    subscript(index: Int) -> Float?
    {
    get
    {
    switch index
    {
    case 0 : return self.x

    case 1 : return self.y

    case 2 : return self.z

    default: return nil
    }
    }
    }

    subscript(index: String) -> Float?
    {
    get
    {
    switch index
    {
    case "x" : return self.x

    case "y" : return self.y

    case "z" : return self.z

    default: return nil
    }
    }
    }
    }

    func +(lhs: Vector, rhs: Vector) -> Vector
    {
    return Vector(x: lhs.x + rhs.x, y: lhs.y + rhs.y, z: lhs.z + rhs.z)
    }

    func *(lhs: Vector, rhs: Float) -> Vector
    {
    return Vector(x: lhs.x * rhs, y: lhs.y * rhs, z: lhs.z * rhs)
    }

    8--)


  • @Jérémy : merci pour tes explications :-)


    @Joana : merci aussi ;-)


  • JérémyJérémy Membre
    février 2017 modifié #20


    Jérémy. Je ne sais pas ce que tu crois que tu fasses  ::)


     


    On ne ni add, ni multiply, les coordonnées d'une pointe. En plus, là , il vaut mieux d'utiliser une struct qu'une classe




     


    C'est pour ça que j'ai dit :


     




    Exemple (le code est complètement débile mais c'est pour illustrer ce que je viens de dire) :




     


    ::)  ::)  ::) (je suis pardonné maintenant ?  :P )


     




    @Jérémy : merci pour tes explications :-)


    @Joana : merci aussi ;-)




     


    De rien 




  • Ou, pour jouer plus avec le vecteur de Céroce :


     




    Oui, mais tu retournes des Float?. Il n'y a pas de perte de performance par rapport à  des Float classique, même minime ? L'utilisation de Céroce est assez .. intense, vu qu'il s'agit d'un immense flux de données graphiques.

  • Joanna CarterJoanna Carter Membre, Modérateur

    Oui, mais tu retournes des Float?. Il n'y a pas de perte de performance par rapport à  des Float classique, même minime ? L'utilisation de Céroce est assez .. intense, vu qu'il s'agit d'un immense flux de données graphiques.




    C'est comme beaucoup de subscripts qui renvoie les optionals en cas d'échec comme index hors limites, à  la place de throw
  • PyrohPyroh Membre
    février 2017 modifié #23


    C'est comme beaucoup de subscripts qui renvoie les optionals en cas d'échec comme index hors limites, à  la place de throw




    Oui mais là  non...


     


    Si on prend ce cas précis on est assuré qu'à  chaque fois qu'on va utiliser une instance de Vector on aura les composantes x, y, z. C'est comme ça c'est un vecteur 3D.


     


    Si on ajoute des subscript on sera également assurés que les indices 0, 1, 2 correspondrons respectivement au trois composantes sus-nommées. Pareil pour les clés "x", "y", "z". En dehors de ces indices et clés il n'y a aucune chance qu'aucun autre ne soit valide.


    Dans ce cas là  c'est fatalError sans se poser de question, pareil que pour un index out of bounds sur un Array. Si le codeur essaie de récupérer une quatrième composante c'est qu'il n'a pas compris un truc. 


     


    Faire retourner un Float? dans ce cas reviendrai à  dire : "Je sais pas, essaie, on verra bien ce qu'on a !" alors que pas du tout ! Il n'y a aucune ambiguà¯té à  aucun moment.


    Les optionals sont là  pour assainir le concept de nullité dans les cas qu'on ne peut pas prévoir avant qu'ils ne surviennent. Ici on peut tout prévoir, la clé "w" ne renverra jamais rien, ni au moment de l'écriture du code, ni lors du runtime, jamais. Pareil pour l'indice 6. Alors pas de throw, pas d'optional.


     


    En plus ça va plomber les performances sans raison valable. L'optional binding c'est pas gratos non plus, ça peut être négligeable sur une dizaine de vecteurs mais au delà  on fait clairement de la contre-optimisation et ça craint.


     


    ApreÌ€s il y a le cas Array vs Dictionary. Array renvoie un objet de type Element tandis que Dictionary[key] renvoie un Element?. La raison est simple on peut vérifier le nombre d'éléments contenus dans un Array avec count. On est alors assuré que les indices 0..<count sont valides et reverrons quelque choses parce qu'on sait que l'indice de début est toujours 0 par convention. 


    Dans le cadre du Dictionary on peut aussi savoir combien il y a d'éléments dans la collection mais cette information n'est pas déterminante. Comme les clés ne sont pas comprise dans un range il n'y a aucun moyen de les déduire sans les connaitre. De plus l'intérêt du Dictionary est de pouvoir avoir une collection dynamique dans laquelle l'absence d'un élément est une information en soit. Ici les optionals font alors sens.


     


    Les optionals sont une très bonne chose et sont très pratiques (je vous renvoie au blog d'Ali pour ça) mais il faut vraiment savoir pourquoi et quand les utiliser sinon ça devient vite contre-intuitif.


  • CéroceCéroce Membre, Modérateur

    En plus ça va plomber les performances sans raison valable. L'optional binding c'est pas gratos non plus, ça peut être négligeable sur une dizaine de vecteurs mais au delà  on fait clairement de la contre-optimisation et ça craint.

    Sans compter la lisibilité, à  chaque fois il faudrait une construction if let component = vec[2].

    Je suis d'accord que les optionals ne sont pas appropriés ici. Dans le principe on pourrait lever une exception, mais là  encore, ça obligerait à  utiliser des blocs de try {}. Utiliser une assertion me parait le plus adéquat, d'autant plus que les assertions sont désactivées en Release.


  • Sans compter la lisibilité, à  chaque fois il faudrait une construction if let component = vec[2].

     


     




    f


    Bah non, si tu es certain de l'existence de la donnée, tu peux forcer sa conversion.


    r



    let x = vec[2]!

  • CéroceCéroce Membre, Modérateur
    février 2017 modifié #26
    Si systématiquement, je force l'unwrapping, à  quoi peut bien servir un optional ?!
  • DrakenDraken Membre
    février 2017 modifié #27

    A rien dans ce cas précis. C'est inutile et contre-productif d'utiliser des optionals sur des données dont l'existence est certaine.


    Je pense que Joanna a utilisé un Float? pour simplifier l'écriture du code et éviter le gag du "return Float()" de Pyroh, qui ne sert qu'à  éviter un hurlement de rage d'Xcode.

  • Joanna CarterJoanna Carter Membre, Modérateur

    Si, comme Draken on ne veut pas tuer les poneys, on peut toujours utiliser un enum pour l'index :



    struct Vector
    {
    enum Index : Int
    {
    case x = 0
    case y
    case z
    }

    var x: Float

    var y: Float

    var z: Float

    subscript(index: Index) -> Float?
    {
    get
    {
    switch index
    {
    case .x : return self.x

    case .y : return self.y

    case .z : return self.z
    }
    }
    }
    }

    Donc, on est garanti de ne pas excéder les limites.


  • Je vois pas trop l'intérêt de faire ça :



    vector[x]

    si on peut faire ça :



    vector.x
  • Joanna CarterJoanna Carter Membre, Modérateur

    Car à  la place des Ints pour les indexes, on aurait un type intégral qui garanti de ne pas dépasser les limites


  • CéroceCéroce Membre, Modérateur

    Je vois pas trop l'intérêt de faire ça :


    vector[x]
    si on peut faire ça :

    vector.x


    Extrait de mon code:


    struct BoundingBox {
    let minPoint: Vec3
    let maxPoint: Vec3

    func isHitBy(ray: Ray, distMin: Float, distMax: Float) -> Bool {
    var tmin = distMin
    var tmax = distMax

    for component in 0..<3 {
    let invD = Float(1.0) / ray.direction[component]
    let t0 = (minPoint[component] - ray.origin[component]) * invD
    let t1 = (maxPoint[component] - ray.origin[component]) * invD
    tmin = max(tmin, min(t0, t1))
    tmax = min(tmax, max(t0, t1))

    if tmax <= tmin {
    return false
    }
    }

    return true
    }
    }
    Sans l'accès aux composantes par indice, on doit répéter 3 fois le même code.
Connectez-vous ou Inscrivez-vous pour répondre.