web-dev-qa-db-fra.com

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

Je passais en revue les exercices dans Ruby Koans et j’ai été frappé par le caprice suivant de Ruby que j’ai trouvé 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 array[5,0] n'est-il pas égal à array[4,0]? Y a-t-il une raison pour laquelle le découpage en matrice se comporte de manière étrange lorsque vous commencez par (longueur + 1)th position??

225
Pascal Van Hecke

Le découpage en tranches et l'indexation sont deux opérations différentes, et le problème réside dans le comportement de l'une à l'autre.

Le premier argument de la tranche identifie non pas l'élément mais les espaces 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éments, vous obtenez la fin vide du tableau. Mais il n'y a pas d'index 5, vous ne pouvez donc pas couper à partir de là.

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

175
Amadan

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 en dehors des limites, il retournera nil. Ainsi, dans votre exemple, array[4,0] demande le 4ème élément qui existe, mais demande de renvoyer un tableau de zéro élément. Alors que array[5,0] demande un index hors limites, il renvoie nil. Cela a peut-être plus de sens si vous vous rappelez que la méthode slice renvoie un tableau new, sans modifier la structure de données d'origine.

MODIFIER:

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 égale à 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 classe array.c où la méthode rb_ary_subseq est définie, vous verrez qu'elle renvoie 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 renvoie un tableau vide si le deuxième argument est défini sur zéro. tandis que si 5 est passé, il n’ya pas 5 éléments dans le tableau, il renvoie donc zéro avant que l’argument zéro soit évalué. code ici à la ligne 944.

Je pense qu'il s'agit d'un bug ou du moins imprévisible et non du "principe de moindre surprise". Dans quelques minutes, je soumettrai au moins un patch de test ayant échoué à Ruby Core. 

27
Jed Schneider

Au moins notez que le comportement est cohérent. À partir de 5, tout fonctionne de la même manière. l'étrangeté ne se produit qu'à [4,N].

Peut-être que ce modèle aide, ou peut-être que je suis tout simplement fatigué et que cela n’aide en rien.

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] => []

Au [4,0], nous attrapons la fin du tableau. En fait, je trouverais ça plutôt étrange, pour ce qui est de la beauté des motifs, si le dernier retourne nil. En raison d'un contexte comme celui-ci, 4 est une option acceptable pour le premier paramètre afin que le tableau vide puisse être renvoyé. Une fois que nous avons atteint 5 ans et plus, cependant, la méthode se termine immédiatement par nature d'être totalement et totalement hors limites.

22
Matchu

Cela a du sens quand vous considérez qu'une tranche de tableau peut être une lvalue valide, pas seulement une rvalue:

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]

Cela ne serait pas possible si array[4,0] retournait nil au lieu de []. Cependant, array[5,0] renvoie nil car il est hors limites (l'insertion après le 4ème élément d'un tableau à 4 éléments est significative, mais l'insertion après le 5ème é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 éléments x dans array, sélectionnez un nombre maximal d'éléments y". Cela n'a de sens que si array a au moins x éléments.

12
Frank Szczerba

Cette fait sens

Vous devez pouvoir affecter ces tranches afin qu'elles soient définies de manière à ce que le début et la fin de la chaîne aient des expressions de longueur zéro actives.

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

Les explications de Gary Wright ont également été très utiles. http://www.Ruby-forum.com/topic/1393096#990065

La réponse de Gary Wright est -

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

La documentation pourrait certainement être plus claire, mais le comportement réel est cohérent et utile. Remarque: je suppose la version 1.9.X de String.

Il est utile d’envisager 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 commune (et compréhensible) est aussi de supposer que la sémantique de l’index à argument unique est la même que celle de l’argument first ​​dans le scénario (ou la plage) à deux arguments. Ce n'est pas la même chose dans la pratique et la documentation ne reflète pas cela. L'erreur cependant est certainement dans la documentation et non dans l'implémentation:

argument unique: l'index représente une position de caractère unique dans la chaîne. Le résultat est soit la chaîne de caractère 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, les parties de largeur nulle de la chaîne peuvent également être identifiées, de sorte que le texte puisse être inséré avant ou après les caractères existants, y compris au début ou à la fin de la chaîne. Dans ce cas, le premier argument not ​​identifie une position de caractère mais identifie plutôt 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 gamme est assez intéressant. Le point de départ est identique au premier argument lorsque deux arguments sont fournis (comme décrit ci-dessus), mais le point final de la plage peut être la "position du caractère" comme avec l'indexation simple ou la "position du bord" comme avec 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 en arrière sur ces exemples et insistez pour que vous utilisiez la sémantique à un seul index pour les exemples d'indexation à deux ou de plages, vous allez vous perdre. Vous devez utiliser la numérotation alternative que je montre dans le diagramme ascii pour modéliser le comportement réel.

8
vim

Je conviens que cela semble être un comportement étrange, mais même la documentation officielle sur Array#slice présente 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#slice ne semble offrir aucun aperçu quant à pourquoi cela fonctionne de la manière suivante:

Référence d'élément - Renvoie l'élément situé à index ou un sous-tableau commençant par start et continuant pour les éléments length, ou un sous-tableau spécifié par range. Les index négatifs comptent à rebours à partir de la fin du tableau (-1 est le dernier élément). Renvoie la valeur nil si l'index (ou l'index de départ) est en dehors de la plage. 

8
Mark Rushakoff

Une explication fournie par Jim Weirich  

Une façon d’y réfléchir est que la position 4 de l’indice se situe à la limite. du tableau. En demandant une tranche, vous retournez autant de tableau qui est laissé. Alors considérons les tableaux [2,10], tableau [3,10] et tableau [4,10] ... chacun renvoie les bits restants de la fin du fichier tableau: 2 éléments, 1 élément et 0 éléments respectivement. Toutefois, la position 5 est clairement en dehors de le tableau et non au bord, donc tableau [5,10] renvoie zéro.

7
suvankar

Considérez le tableau suivant:

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

Vous pouvez insérer un élément au début (en tête) du tableau en l'attribuant à a[0,0]. Pour placer l'élément entre "a" et "b", utilisez a[1,0]. Fondamentalement, dans la notation a[i,n], i représente un index et n un nombre d'éléments. Lorsque n=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]. C'est la queue du tableau. 

Donc, si vous essayez d'accéder à l'élément en 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 nil comme valeur de retour, car vous ne vous trouvez plus dans la plage du tableau.

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

6
Tairone