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?
Respostas:
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):
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.fonte
isso tem a ver com o fato de que slice retorna uma matriz, documentação de origem relevante da matriz Array # slice:
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. Enquantoarray[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:
se você procurar na
array.c
classe em que orb_ary_subseq
método está definido, verá que ele retornará nulo se o comprimento estiver fora dos limites, não o índice: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.
fonte
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.
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 voltassenil
. 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.fonte
Isso faz sentido quando você considera que uma fatia da matriz pode ser um lvalue válido, não apenas um rvalue:
Isso não seria possível se
array[4,0]
retornado emnil
vez de[]
. No entanto,array[5,0]
retornanil
porque 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ósx
elementosarray
, selecione atéy
elementos". Isso é significativo apenas searray
tiver pelo menosx
elementos.fonte
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.
fonte
array[5,0]=:foo # array is now [:peanut, :butter, :and, :jelly, nil, :foo]
[26] pry(main)> array[4,5] = [:love, :hope, :peace] => [:peanut, :butter, :and, :jelly, :love, :hope, :peace]
array = [:a, :b, :c, :d, :e]; array[1,2] = :x, :x; array => [:a, :x, :x, :d, :e]
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:
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.
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.
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:
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.
fonte
Concordo que isso parece um comportamento estranho, mas mesmo a documentação oficial
Array#slice
demonstra o mesmo comportamento do seu exemplo, nos "casos especiais" abaixo:Infelizmente, mesmo a descrição de
Array#slice
não parece oferecer uma idéia do porquê funciona dessa maneira:fonte
Uma explicação fornecida por Jim Weirich
fonte
Considere a seguinte matriz:
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"
, usea[1,0]
. Basicamente, na notaçãoa[i,n]
,i
representa um índice en
vários elementos. Quandon=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 acessara[4,0]
, obterá onil
valor 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/ .
fonte
tl; dr: no código fonte
array.c
, funções diferentes são chamadas, dependendo de você passar 1 ou 2 argumentos paraArray#slice
resultar 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_entry
calcula 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 chamarb_ary_elt
para obter o elemento solicitado.Como esperado,
rb_ary_elt
retornanil
quando o comprimento da matrizlen
é menor ou igual ao índice (aqui chamadooffset
).Cenário # 2
array.length == 4; array.slice(4, 0) #=> []
No entanto, quando dois argumentos são passados (isto é, o índice inicial
beg
e o comprimento da fatialen
),rb_ary_subseq
é chamado.Em
rb_ary_subseq
, se o índice inicialbeg
for maior que o comprimento da matrizalen
,nil
será retornado:Caso contrário, o comprimento da fatia resultante
len
será calculado e, se for determinado como zero, uma matriz vazia será retornada:Portanto, como o índice inicial de 4 não é maior que
array.length
, uma matriz vazia é retornada em vez donil
valor 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.
fonte