Corte de matriz em Ruby: explicação para comportamento ilógico (extraído de Rubykoans.com)

232

Eu estava fazendo os exercícios em Ruby Koans e fiquei impressionado com a seguinte peculiaridade de Ruby que achei realmente inexplicável:

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...

Então, por que array[5,0]não é igual a array[4,0]? Existe alguma razão pela qual o fatiamento de array se comporta de maneira estranha quando você começa na (comprimento + 1) th posição?

Pascal Van Hecke
fonte
parece que o primeiro número é o índice para começar, o segundo número é quantos elementos fatiar #
austin

Respostas:

185

Fatiar e indexar são duas operações diferentes, e inferir o comportamento de uma delas é onde está o seu problema.

O primeiro argumento na fatia identifica não o elemento, mas os lugares entre os elementos, definindo extensões (e não os próprios elementos):

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

4 ainda está dentro da matriz, apenas um pouco; se você solicitar 0 elementos, obterá o final vazio da matriz. Mas não há índice 5, então você não pode cortar a partir daí.

Quando você indexa (como array[4]), está apontando os próprios elementos, portanto os índices vão apenas de 0 a 3.

Amadan
fonte
8
Um bom palpite, a menos que seja feito backup pela fonte. Não sendo irritante, eu estaria interessado em um link, se houver, apenas para explicar o "porquê", como o OP e outros comentaristas estão perguntando. Seu diagrama faz sentido, exceto que Matriz [4] é nula. A matriz [3] é: geléia. Eu esperaria que a Matriz [4, N] fosse nula, mas é como o OP diz. Se é um lugar, é um lugar bastante inútil porque a Matriz [4, -1] é nula. Então você não pode fazer nada com o Array [4].
squarism
5
Acabei de receber uma confirmação de Charles Oliver Nutter (@headius no Twitter) de que esta é a explicação correta. Ele é um grande desenvolvedor de JRuby, então eu consideraria sua palavra bastante autoritária.
Hank Gay
18
A seguir, é apresentada a justificativa para esse comportamento: blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-talk/380637
Matt Briançon
4
Explicação correta. Discussões similares sobre ruby-core: redmine.ruby-lang.org/issues/4245 , redmine.ruby-lang.org/issues/4541
Marc-André Lafortune
18
Também conhecido como "postagem de cerca". O quinto poste (id 4) existe, mas o quinto elemento não. O fatiamento é uma operação de pilar, a indexação é uma operação de elemento.
Matty K
27

isso tem a ver com o fato de que slice retorna uma matriz, documentação de origem relevante da matriz 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

o que me sugere que, se você der um início fora dos limites, ele retornará nulo, portanto, no seu exemplo, array[4,0]solicita o quarto elemento que existe, mas solicita o retorno de uma matriz de zero elementos. Enquanto array[5,0]solicita um índice fora dos limites, ele retorna nulo. Talvez isso faça mais sentido se você se lembrar de que o método de fatia está retornando uma nova matriz, sem alterar a estrutura de dados original.

EDITAR:

Depois de revisar os comentários, decidi editar esta resposta. A fatia chama o seguinte snippet de código quando o valor arg é dois:

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);
}

se você procurar na array.cclasse em que o rb_ary_subseqmétodo está definido, verá que ele retornará nulo se o comprimento estiver fora dos limites, não o índice:

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

Nesse caso, é o que está acontecendo quando o 4 é passado, verifica se há 4 elementos e, portanto, não aciona o retorno nulo. Em seguida, ele continua e retorna uma matriz vazia se o segundo argumento estiver definido como zero. enquanto se 5 é passado, não há 5 elementos na matriz, portanto, ele retorna nulo antes que o zero arg seja avaliado. código aqui na linha 944.

Eu acredito que isso seja um bug, ou pelo menos imprevisível e não o 'Princípio da menor surpresa'. Quando eu tiver alguns minutos, enviarei pelo menos um patch de teste com falha ao ruby ​​core.

Jed Schneider
fonte
2
Mas ... o elemento indicado pelo 4 na matriz [4,0] também não existe ... - porque na verdade é o 5o elemento (contagem baseada em 0, veja os exemplos). Portanto, está fora dos limites também.
Pascal Van Hecke
1
você está certo. Voltei e olhei para a fonte, e parece que o primeiro argumento é tratado dentro do código c como o comprimento, não o índice. Vou editar minha resposta, para refletir isso. Eu acho que isso pode ser enviado como um bug.
Jed Schneider
23

Pelo menos observe que o comportamento é consistente. De 5 em diante tudo age da mesma maneira; a estranheza ocorre apenas às [4,N].

Talvez esse padrão ajude, ou talvez eu esteja cansado e não ajude nada.

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

Em [4,0], pegamos o final da matriz. Na verdade, eu achava isso bastante estranho, no que diz respeito à beleza dos padrões, se o último voltasse nil. Devido a um contexto como esse, 4é uma opção aceitável para o primeiro parâmetro, para que a matriz vazia possa ser retornada. Uma vez que atingimos 5 ou mais, o método provavelmente sai imediatamente por natureza, total e completamente fora dos limites.

Matchu
fonte
12

Isso faz sentido quando você considera que uma fatia da matriz pode ser um lvalue válido, não apenas um 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]

Isso não seria possível se array[4,0]retornado em nilvez de []. No entanto, array[5,0]retorna nilporque está fora dos limites (inserir após o quarto elemento de uma matriz de 4 elementos é significativo, mas inserir após o 5º elemento de uma matriz de 4 elementos não é).

Leia a sintaxe da fatia array[x,y]como "iniciando após xelementos array, selecione até yelementos". Isso é significativo apenas se arraytiver pelo menos xelementos.

Frank Szczerba
fonte
11

Isso faz sentido

Você precisa ser capaz de atribuir a essas fatias, para que sejam definidas de forma que o início e o fim da sequência tenham expressões de comprimento zero.

array[4, 0] = :sandwich
array[0, 0] = :crunchy
=> [:crunchy, :peanut, :butter, :and, :jelly, :sandwich]
DigitalRoss
fonte
1
Você também pode atribuir ao intervalo a fatia que retorna como nula; portanto, seria útil expandir essa explicação. array[5,0]=:foo # array is now [:peanut, :butter, :and, :jelly, nil, :foo]
mfazekas
o que o segundo número faz ao atribuir? parece ser ignorado. [26] pry(main)> array[4,5] = [:love, :hope, :peace] => [:peanut, :butter, :and, :jelly, :love, :hope, :peace]
precisa
@drewverlee que não é ignorado:array = [:a, :b, :c, :d, :e]; array[1,2] = :x, :x; array => [:a, :x, :x, :d, :e]
fanaugen
10

Achei a explicação de Gary Wright muito útil também. http://www.ruby-forum.com/topic/1393096#990065

A resposta de Gary Wright é -

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

Os documentos certamente poderiam ser mais claros, mas o comportamento real é autoconsistente e útil. Nota: Estou assumindo a versão 1.9.X do String.

Ajuda a considerar a numeração da seguinte maneira:

  -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

O erro comum (e compreensível) também é assumir que a semântica do índice de argumento único é igual à semântica do primeiro argumento no cenário de dois argumentos (ou intervalo). Eles não são a mesma coisa na prática e a documentação não reflete isso. O erro, porém, está definitivamente na documentação e não na implementação:

argumento único: o índice representa uma posição de caractere único na cadeia. O resultado é a sequência de caracteres únicos encontrada no índice ou nula porque não há caracteres no índice fornecido.

  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

dois argumentos inteiros: os argumentos identificam uma parte da sequência a ser extraída ou substituída. Em particular, partes da cadeia com largura zero também podem ser identificadas para que o texto possa ser inserido antes ou depois dos caracteres existentes, inclusive na frente ou no final da cadeia. Nesse caso, o primeiro argumento não identifica uma posição de caractere, mas identifica o espaço entre os caracteres, conforme mostrado no diagrama acima. O segundo argumento é o comprimento, que pode ser 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"

O comportamento de um intervalo é bastante interessante. O ponto inicial é o mesmo que o primeiro argumento quando dois argumentos são fornecidos (como descrito acima), mas o ponto final do intervalo pode ser a 'posição do caractere' como na indexação única ou a "posição da aresta" como com dois argumentos inteiros. A diferença é determinada pelo uso do intervalo de pontos duplos ou triplo:

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.

Se você voltar a esses exemplos e insistir e usar a semântica de índice único para os exemplos de indexação dupla ou de intervalo, ficará confuso. Você precisa usar a numeração alternativa mostrada no diagrama ascii para modelar o comportamento real.

vim
fonte
3
Você pode incluir a ideia principal desse segmento? (no caso do link de uma dia se torna inválido)
VonC
8

Concordo que isso parece um comportamento estranho, mas mesmo a documentação oficialArray#slice demonstra o mesmo comportamento do seu exemplo, nos "casos especiais" abaixo:

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

Infelizmente, mesmo a descrição de Array#slicenão parece oferecer uma idéia do porquê funciona dessa maneira:

Referência do elemento - Retorna o elemento no índice , ou retorna uma sub-matriz iniciando no início e continuando para os elementos de comprimento , ou retorna uma sub-matriz especificada pelo intervalo . Os índices negativos contam para trás a partir do final da matriz (-1 é o último elemento). Retorna nulo se o índice (ou índice inicial) estiver fora da faixa.

Mark Rushakoff
fonte
7

Uma explicação fornecida por Jim Weirich

Uma maneira de pensar é que a posição 4 do índice está na extremidade da matriz. Ao pedir uma fatia, você retorna a maior parte da matriz que resta. Portanto, considere a matriz [2,10], matriz [3,10] e matriz [4,10] ... cada uma retorna os bits restantes do final da matriz: 2 elementos, 1 elemento e 0 elementos, respectivamente. No entanto, a posição 5 está claramente fora da matriz e não na borda, portanto a matriz [5,10] retorna nulo.

suvankar
fonte
6

Considere a seguinte matriz:

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

Você pode inserir um item no início (cabeçalho) da matriz, atribuindo-o a a[0,0]. Para colocar o elemento entre "a"e "b", use a[1,0]. Basicamente, na notação a[i,n], irepresenta um índice e nvários elementos. Quando n=0, define uma posição entre os elementos da matriz.

Agora, se você pensar no final da matriz, como anexar um item ao final usando a notação descrita acima? Simples, atribua o valor a a[3,0]. Este é o final da matriz.

Portanto, se você tentar acessar o elemento em a[3,0], receberá []. Nesse caso, você ainda está no intervalo da matriz. Mas se você tentar acessar a[4,0], obterá o nilvalor de retorno, já que não está mais dentro do intervalo da matriz.

Leia mais sobre isso em http://mybrainstormings.wordpress.com/2012/09/10/arrays-in-ruby/ .

Tairone
fonte
0

tl; dr: no código fonte array.c, funções diferentes são chamadas, dependendo de você passar 1 ou 2 argumentos para Array#sliceresultar em valores de retorno inesperados.

(Primeiro, gostaria de salientar que não codigo em C, mas uso Ruby há anos. Portanto, se você não está familiarizado com C, mas leva alguns minutos para se familiarizar com o básico de funções e variáveis, não é tão difícil seguir o código fonte do Ruby, como demonstrado abaixo. Essa resposta é baseada no Ruby v2.3, mas é mais ou menos a mesma coisa na v1.9.)

Cenário 1

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

Se você olhar para o código-fonte Array#slice( rb_ary_aref), verá que quando apenas um argumento é passado ( linhas 1277-1289 ), rb_ary_entryé chamado, passando o valor do índice (que pode ser positivo ou negativo).

rb_ary_entrycalcula a posição do elemento solicitado desde o início da matriz (em outras palavras, se um índice negativo é passado, ele calcula o equivalente positivo) e depois chama rb_ary_eltpara obter o elemento solicitado.

Como esperado, rb_ary_eltretorna nilquando o comprimento da matriz lené menor ou igual ao índice (aqui chamado offset).

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

Cenário # 2

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

No entanto, quando dois argumentos são passados ​​(isto é, o índice inicial bege o comprimento da fatia len), rb_ary_subseqé chamado.

Em rb_ary_subseq, se o índice inicial begfor maior que o comprimento da matriz alen, nilserá retornado:

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

Caso contrário, o comprimento da fatia resultante lenserá calculado e, se for determinado como zero, uma matriz vazia será retornada:

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);

Portanto, como o índice inicial de 4 não é maior que array.length, uma matriz vazia é retornada em vez do nilvalor esperado.

Pergunta respondida?

Se a pergunta real aqui não for "Qual código faz com que isso aconteça?", Mas "Por que Matz fez dessa maneira?", Bem, você terá que comprar uma xícara de café para ele no próximo RubyConf e pergunte a ele.

Scott Schupbach
fonte