Découpage de tableaux en Ruby: explication d'un comportement illogique (extrait de Rubykoans.com)


232

Je suivais les exercices de Ruby Koans et j'ai été frappé par la bizarrerie de Ruby suivante que je trouvais vraiment inexplicable:

array = [:peanut, :butter, :and, :jelly]

array[0]     #=> :peanut    #OK!
array[0,1]   #=> [:peanut]  #OK!
array[0,2]   #=> [:peanut, :butter]  #OK!
array[0,0]   #=> []    #OK!
array[2]     #=> :and  #OK!
array[2,2]   #=> [:and, :jelly]  #OK!
array[2,20]  #=> [:and, :jelly]  #OK!
array[4]     #=> nil  #OK!
array[4,0]   #=> []   #HUH??  Why's that?
array[4,100] #=> []   #Still HUH, but consistent with previous one
array[5]     #=> nil  #consistent with array[4] #=> nil  
array[5,0]   #=> nil  #WOW.  Now I don't understand anything anymore...

Alors pourquoi n'est array[5,0]pas égal à array[4,0]? Y a-t-il une raison pour laquelle le découpage de réseau se comporte aussi bizarrement lorsque vous commencez à la (longueur + 1) ème position ??



on dirait que le premier nombre est l'indice de départ, le deuxième nombre est le nombre d'éléments à découper
austin

Réponses:


185

Le découpage et l'indexation sont deux opérations différentes, et inférer le comportement de l'un à partir de l'autre est là où réside votre problème.

Le premier argument de la tranche identifie non pas l'élément mais les emplacements entre les éléments, définissant les étendues (et non les éléments eux-mêmes):

  :peanut   :butter   :and   :jelly
0         1         2      3        4

4 est toujours dans le tableau, à peine; si vous demandez 0 élément, vous obtenez l'extrémité vide du tableau. Mais il n'y a pas d'index 5, vous ne pouvez donc pas découper à partir de là.

Lorsque vous indexez (comme array[4]), vous pointez sur les éléments eux-mêmes, donc les index ne vont que de 0 à 3.


8
Une bonne supposition à moins que cela ne soit sauvegardé par la source. N'étant pas sarcastique, je serais intéressé par un lien s'il y en a juste pour expliquer le "pourquoi" comme le PO et d'autres commentateurs le demandent. Votre diagramme a du sens, sauf que Array [4] est nul. Le tableau [3] est: gelée. Je m'attendrais à ce que Array [4, N] soit nul mais c'est [] comme le dit l'OP. Si c'est un endroit, c'est un endroit assez inutile car Array [4, -1] est nul. Vous ne pouvez donc rien faire avec Array [4].
squarisme

5
@squarism Je viens de recevoir la confirmation de Charles Oliver Nutter (@headius sur Twitter) que c'est la bonne explication. C'est un grand développeur de JRuby, donc je considérerais son mot comme faisant autorité.
Hank Gay

18

4
Explication correcte. Discussions similaires sur ruby-core: redmine.ruby-lang.org/issues/4245 , redmine.ruby-lang.org/issues/4541
Marc-André Lafortune

18
Également appelé «affichage de clôture». Le cinquième poteau de clôture (id 4) existe, mais le cinquième élément n'existe pas. Le découpage est une opération de poteau de clôture, l'indexation est une opération d'élément.
Matty K

27

cela a à voir avec le fait que slice renvoie un tableau, la documentation source pertinente de Array # slice:

 *  call-seq:
 *     array[index]                -> obj      or nil
 *     array[start, length]        -> an_array or nil
 *     array[range]                -> an_array or nil
 *     array.slice(index)          -> obj      or nil
 *     array.slice(start, length)  -> an_array or nil
 *     array.slice(range)          -> an_array or nil

ce qui me suggère que si vous donnez le début qui est hors limites, il retournera zéro, donc dans votre exemple array[4,0]demande le 4ème élément qui existe, mais demande de retourner un tableau de zéro éléments. Tandis que array[5,0]demande un index hors limites, il renvoie donc zéro. Cela est peut-être plus logique si vous vous souvenez que la méthode slice renvoie un nouveau tableau, sans altérer la structure de données d'origine.

ÉDITER:

Après avoir examiné les commentaires, j'ai décidé de modifier cette réponse. Slice appelle l' extrait de code suivant lorsque la valeur arg est deux:

if (argc == 2) {
    if (SYMBOL_P(argv[0])) {
        rb_raise(rb_eTypeError, "Symbol as array index");
    }
    beg = NUM2LONG(argv[0]);
    len = NUM2LONG(argv[1]);
    if (beg < 0) {
        beg += RARRAY(ary)->len;
    }
    return rb_ary_subseq(ary, beg, len);
}

si vous regardez dans la array.cclasse où la rb_ary_subseqméthode est définie, vous voyez qu'elle retourne nil si la longueur est hors limites, pas l'index:

if (beg > RARRAY_LEN(ary)) return Qnil;

Dans ce cas, c'est ce qui se passe lorsque 4 est passé, il vérifie qu'il y a 4 éléments et ne déclenche donc pas le retour nul. Il continue ensuite et retourne un tableau vide si le deuxième argument est défini sur zéro. tandis que si 5 est passé, il n'y a pas 5 éléments dans le tableau, donc il retourne nil avant que l'argument zéro soit évalué. code ici à la ligne 944.

Je pense que c'est un bug, ou du moins imprévisible et non le «principe de la moindre surprise». Quand j'aurai quelques minutes, je soumettrai au moins un patch de test défaillant à ruby ​​core.


2
Mais ... l'élément indiqué par le 4 dans le tableau [4,0] n'existe pas non plus ... - car il s'agit en fait de l'élément 5the (comptage basé sur 0, voir les exemples). Il est donc également interdit.
Pascal Van Hecke

1
vous avez raison. Je suis retourné en arrière et j'ai regardé la source, et il semble que le premier argument soit traité dans le code c comme la longueur, pas l'index. Je vais modifier ma réponse pour refléter cela. Je pense que cela pourrait être soumis comme un bug.
Jed Schneider

23

Notez au moins que le comportement est cohérent. À partir de 5 heures, tout agit de la même façon; l'étrangeté ne se produit qu'à [4,N].

Peut-être que ce schéma aide, ou peut-être que je suis juste fatigué et que cela n'aide pas du tout.

array[0,4] => [:peanut, :butter, :and, :jelly]
array[1,3] => [:butter, :and, :jelly]
array[2,2] => [:and, :jelly]
array[3,1] => [:jelly]
array[4,0] => []

À [4,0], nous attrapons la fin du tableau. Je trouverais en fait plutôt étrange, en ce qui concerne la beauté des motifs, si le dernier revenait nil. En raison d'un contexte comme celui-ci, 4est une option acceptable pour le premier paramètre afin que le tableau vide puisse être retourné. Une fois que nous avons atteint 5 et plus, cependant, la méthode sort probablement immédiatement par nature d'être totalement et complètement hors des limites.


12

Cela a du sens lorsque vous considérez qu'une tranche de tableau peut être une valeur l valide, et pas seulement une valeur r:

array = [:peanut, :butter, :and, :jelly]
# replace 0 elements starting at index 5 (insert at end or array):
array[4,0] = [:sandwich]
# replace 0 elements starting at index 0 (insert at head of array):
array[0,0] = [:make, :me, :a]
# array is [:make, :me, :a, :peanut, :butter, :and, :jelly, :sandwich]

# this is just like replacing existing elements:
array[3, 4] = [:grilled, :cheese]
# array is [:make, :me, :a, :grilled, :cheese, :sandwich]

Ce ne serait pas possible si array[4,0]renvoyé à la nilplace de []. Cependant, array[5,0]renvoie nilcar il est hors limites (l'insertion après le 4e élément d'un tableau à 4 éléments est significative, mais l'insertion après le 5e élément d'un tableau à 4 éléments ne l'est pas).

Lisez la syntaxe de tranche array[x,y]comme "commençant après les xéléments dans array, sélectionnez jusqu'aux yéléments". Cela n'a de sens que s'il arraycontient au moins des xéléments.


11

Cela ne du sens

Vous devez être en mesure d'affecter à ces tranches, afin qu'elles soient définies de telle sorte que le début et la fin de la chaîne aient des expressions de longueur nulle.

array[4, 0] = :sandwich
array[0, 0] = :crunchy
=> [:crunchy, :peanut, :butter, :and, :jelly, :sandwich]

1
Vous pouvez également affecter à la plage cette tranche qui renvoie la valeur zéro, il serait donc utile de développer cette explication. array[5,0]=:foo # array is now [:peanut, :butter, :and, :jelly, nil, :foo]
mfazekas

que fait le deuxième numéro lors de l'attribution? il semble être ignoré. [26] pry(main)> array[4,5] = [:love, :hope, :peace] => [:peanut, :butter, :and, :jelly, :love, :hope, :peace]
Drew Verlee

@drewverlee il n'est pas ignoré:array = [:a, :b, :c, :d, :e]; array[1,2] = :x, :x; array => [:a, :x, :x, :d, :e]
fanaugen

10

J'ai également trouvé l'explication de Gary Wright très utile. http://www.ruby-forum.com/topic/1393096#990065

La réponse de Gary Wright est -

http://www.ruby-doc.org/core/classes/Array.html

Les documents pourraient certainement être plus clairs, mais le comportement réel est cohérent et utile. Remarque: je suppose que la version 1.9.X de String.

Il est utile de considérer la numérotation de la manière suivante:

  -4  -3  -2  -1    <-- numbering for single argument indexing
   0   1   2   3
 +---+---+---+---+
 | a | b | c | d |
 +---+---+---+---+
 0   1   2   3   4  <-- numbering for two argument indexing or start of range
-4  -3  -2  -1

L'erreur courante (et compréhensible) est également de supposer que la sémantique de l'index à argument unique est la même que la sémantique du premier argument dans le scénario (ou plage) à deux arguments. Ce n'est pas la même chose dans la pratique et la documentation ne reflète pas cela. L'erreur est cependant définitivement dans la documentation et non dans l'implémentation:

seul argument: l'index représente une position de caractère unique dans la chaîne. Le résultat est soit la chaîne de caractères unique trouvée à l'index, soit nil car il n'y a pas de caractère à l'index donné.

  s = ""
  s[0]    # nil because no character at that position

  s = "abcd"
  s[0]    # "a"
  s[-4]   # "a"
  s[-5]   # nil, no characters before the first one

deux arguments entiers: les arguments identifient une partie de la chaîne à extraire ou à remplacer. En particulier, des parties de largeur nulle de la chaîne peuvent également être identifiées afin que le texte puisse être inséré avant ou après les caractères existants, y compris à l'avant ou à la fin de la chaîne. Dans ce cas, le premier argument n'identifie pas une position de caractère mais identifie à la place l'espace entre les caractères comme indiqué dans le diagramme ci-dessus. Le deuxième argument est la longueur, qui peut être 0.

s = "abcd"   # each example below assumes s is reset to "abcd"

To insert text before 'a':   s[0,0] = "X"           #  "Xabcd"
To insert text after 'd':    s[4,0] = "Z"           #  "abcdZ"
To replace first two characters: s[0,2] = "AB"      #  "ABcd"
To replace last two characters:  s[-2,2] = "CD"     #  "abCD"
To replace middle two characters: s[1..3] = "XX"    #  "aXXd"

Le comportement d'une plage est assez intéressant. Le point de départ est le même que le premier argument lorsque deux arguments sont fournis (comme décrit ci-dessus) mais le point final de la plage peut être la «position de caractère» comme pour l'indexation simple ou la «position de bord» comme pour deux arguments entiers. La différence est déterminée par l'utilisation de la plage à deux points ou de la plage à trois points:

s = "abcd"
s[1..1]           # "b"
s[1..1] = "X"     # "aXcd"

s[1...1]          # ""
s[1...1] = "X"    # "aXbcd", the range specifies a zero-width portion of
the string

s[1..3]           # "bcd"
s[1..3] = "X"     # "aX",  positions 1, 2, and 3 are replaced.

s[1...3]          # "bc"
s[1...3] = "X"    # "aXd", positions 1, 2, but not quite 3 are replaced.

Si vous revenez à travers ces exemples et insistez pour utiliser la sémantique d'index unique pour les exemples d'indexation double ou plage, vous serez simplement confus. Vous devez utiliser la numérotation alternative que je montre dans le diagramme ascii pour modéliser le comportement réel.


3
Pouvez-vous inclure l'idée principale de ce fil? (dans le cas où le lien devient invalide un jour)
VonC

8

Je suis d'accord que cela semble étrange, mais même la documentation officielle surArray#slice démontre le même comportement que dans votre exemple, dans les "cas spéciaux" ci-dessous:

   a = [ "a", "b", "c", "d", "e" ]
   a[2] +  a[0] + a[1]    #=> "cab"
   a[6]                   #=> nil
   a[1, 2]                #=> [ "b", "c" ]
   a[1..3]                #=> [ "b", "c", "d" ]
   a[4..7]                #=> [ "e" ]
   a[6..10]               #=> nil
   a[-3, 3]               #=> [ "c", "d", "e" ]
   # special cases
   a[5]                   #=> nil
   a[5, 1]                #=> []
   a[5..10]               #=> []

Malheureusement, même leur description de Array#slicene semble pas donner de raison pour laquelle cela fonctionne de cette façon:

Référence de l'élément: renvoie l'élément à l' index ou renvoie un sous-tableau commençant au début et se poursuivant pendant éléments de longueur , ou retourne un sous-tableau spécifié par plage . Les indices négatifs comptent à rebours à partir de la fin du tableau (-1 est le dernier élément). Renvoie nil si l'index (ou l'index de départ) est hors limites.


7

Une explication fournie par Jim Weirich

Une façon d'y penser est que la position d'index 4 est tout au bord du tableau. Lorsque vous demandez une tranche, vous retournez autant de tableau qui reste. Considérez donc le tableau [2,10], le tableau [3,10] et le tableau [4,10] ... chacun renvoie les bits restants de la fin du tableau: 2 éléments, 1 élément et 0 élément respectivement. Cependant, la position 5 est clairement en dehors du tableau et non sur le bord, donc le tableau [5,10] renvoie zéro.


6

Considérez le tableau suivant:

>> array=["a","b","c"]
=> ["a", "b", "c"]

Vous pouvez insérer un élément au début (tête) du tableau en l'affectant à a[0,0]. Pour mettre l'élément entre "a"et "b", utilisez a[1,0]. Fondamentalement, dans la notation a[i,n], ireprésente un index et nun certain nombre d'éléments. Quandn=0 , il définit une position entre les éléments du tableau.

Maintenant, si vous pensez à la fin du tableau, comment pouvez-vous ajouter un élément à sa fin en utilisant la notation décrite ci-dessus? Simple, attribuez la valeur à a[3,0]. Ceci est la queue du tableau.

Donc, si vous essayez d'accéder à l'élément à a[3,0], vous obtiendrez []. Dans ce cas, vous êtes toujours dans la plage du tableau. Mais si vous essayez d'accéder a[4,0], vous obtiendrez une nilvaleur de retour, car vous n'êtes plus dans la plage du tableau.

En savoir plus à ce sujet sur http://mybrainstormings.wordpress.com/2012/09/10/arrays-in-ruby/ .


0

tl; dr: dans le code source de array.c, différentes fonctions sont appelées selon que vous passez 1 ou 2 arguments pour Array#sliceaboutir à des valeurs de retour inattendues.

(Tout d'abord, je voudrais souligner que je ne code pas en C, mais que j'utilise Ruby depuis des années. Donc, si vous n'êtes pas familier avec C, mais vous prenez quelques minutes pour vous familiariser avec les bases des fonctions et des variables, ce n'est vraiment pas si difficile de suivre le code source de Ruby, comme illustré ci-dessous. Cette réponse est basée sur Ruby v2.3, mais est plus ou moins la même que celle de v1.9.)

Scénario 1

array.length == 4; array.slice(4) #=> nil

Si vous regardez le code source de Array#slice( rb_ary_aref), vous voyez que lorsqu'un seul argument est passé ( lignes 1277-1289 ), rb_ary_entryest appelé, en passant la valeur d'index (qui peut être positive ou négative).

rb_ary_entrycalcule ensuite la position de l'élément demandé depuis le début du tableau (en d'autres termes, si un index négatif est transmis, il calcule l'équivalent positif), puis appelle rb_ary_eltpour obtenir l'élément demandé.

Comme prévu, rb_ary_eltretourne nillorsque la longueur du tableau lenest inférieure ou égale à l'index (appelé ici offset).

1189:  if (offset < 0 || len <= offset) {
1190:    return Qnil;
1191:  } 

Scénario # 2

array.length == 4; array.slice(4, 0) #=> []

Cependant, lorsque 2 arguments sont passés (c'est-à-dire l'indice de départ beget la longueur de la tranche len), rb_ary_subseqest appelé.

Dans rb_ary_subseq, si l'index de départ begest supérieur à la longueur du tableau alen, nilest retourné:

1208:  long alen = RARRAY_LEN(ary);
1209:
1210:  if (beg > alen) return Qnil;

Sinon, la longueur de la tranche résultante lenest calculée, et s'il est déterminé qu'elle est nulle, un tableau vide est renvoyé:

1213:  if (alen < len || alen < beg + len) {
1214:  len = alen - beg;
1215:  }
1216:  klass = rb_obj_class(ary);
1217:  if (len == 0) return ary_new(klass, 0);

Ainsi, puisque l'index de départ de 4 n'est pas supérieur à array.length, un tableau vide est renvoyé à la place de la nilvaleur que l'on pourrait attendre.

Réponse à la question?

Si la vraie question ici n'est pas "Quel code provoque cela?", Mais plutôt "Pourquoi Matz l'a-t-il fait de cette façon?", Vous n'aurez qu'à lui acheter une tasse de café lors du prochain RubyConf et demande lui.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.