J'essaie de comprendre les blocs et yield
et leur fonctionnement en Ruby.
Comment yield
est-il utilisé? La plupart des applications Rails que j'ai examinées utilisent yield
de manière étrange.
Quelqu'un peut-il m'expliquer ou me montrer où aller pour les comprendre?
Oui, c'est un peu déroutant au début.
En Ruby, les méthodes peuvent recevoir un bloc de code afin d’exécuter des segments de code arbitraires.
Lorsqu'une méthode attend un bloc, elle l'invoque en appelant la fonction yield
.
C'est très pratique, par exemple, pour parcourir une liste ou pour fournir un algorithme personnalisé.
Prenons l'exemple suivant:
Je vais définir une classe Person
initialisée avec un nom et fournir une méthode do_with_name
qui, une fois appelée, transmettrait simplement l'attribut name
au bloc reçu.
class Person
def initialize( name )
@name = name
end
def do_with_name
yield( @name )
end
end
Cela nous permettrait d'appeler cette méthode et de passer un bloc de code arbitraire.
Par exemple, pour imprimer le nom, nous ferions:
person = Person.new("Oscar")
#invoking the method passing a block
person.do_with_name do |name|
puts "Hey, his name is #{name}"
end
Souhaitez imprimer:
Hey, his name is Oscar
Notez que le bloc reçoit, en tant que paramètre, une variable appelée name
(NB: vous pouvez appeler cette variable comme bon vous semble, mais il est logique de l’appeler name
). Lorsque le code appelle yield
, il remplit ce paramètre avec la valeur @name
.
yield( @name )
Nous pourrions fournir un autre bloc pour effectuer une action différente. Par exemple, inversez le nom:
#variable to hold the name reversed
reversed_name = ""
#invoke the method passing a different block
person.do_with_name do |name|
reversed_name = name.reverse
end
puts reversed_name
=> "racsO"
Nous avons utilisé exactement la même méthode (do_with_name
) - il s’agit simplement d’un bloc différent.
Cet exemple est trivial. Les utilisations les plus intéressantes sont de filtrer tous les éléments d’un tableau:
days = ["monday", "tuesday", "wednesday", "thursday", "friday"]
# select those which start with 't'
days.select do | item |
item.match /^t/
end
=> ["tuesday", "thursday"]
Nous pouvons également fournir un algorithme de tri personnalisé, basé par exemple sur la taille de la chaîne:
days.sort do |x,y|
x.size <=> y.size
end
=> ["monday", "friday", "tuesday", "thursday", "wednesday"]
J'espère que cela vous aide à mieux comprendre.
BTW, si le bloc est optionnel, vous devriez l'appeler comme suit:
yield(value) if block_given?
Si n'est pas facultatif, il suffit de l'invoquer.
En Ruby, les méthodes peuvent vérifier si elles ont été appelées de manière à fournir un bloc en plus des arguments normaux. Cela se fait généralement à l'aide de la méthode block_given?
, mais vous pouvez également faire référence au bloc en tant que Proc explicite en préfixant une esperluette (&
) avant le nom de l'argument final.
Si une méthode est invoquée avec un bloc, elle peut yield
contrôler le bloc (appeler le bloc) avec des arguments, si nécessaire. Considérez cet exemple de méthode qui montre:
def foo(x)
puts "OK: called as foo(#{x.inspect})"
yield("A gift from foo!") if block_given?
end
foo(10)
# OK: called as foo(10)
foo(123) {|y| puts "BLOCK: #{y} How Nice =)"}
# OK: called as foo(123)
# BLOCK: A gift from foo! How Nice =)
Ou, en utilisant la syntaxe d'argument de bloc spéciale:
def bar(x, &block)
puts "OK: called as bar(#{x.inspect})"
block.call("A gift from bar!") if block
end
bar(10)
# OK: called as bar(10)
bar(123) {|y| puts "BLOCK: #{y} How Nice =)"}
# OK: called as bar(123)
# BLOCK: A gift from bar! How Nice =)
Il est fort possible que quelqu'un fournisse une réponse vraiment détaillée ici, mais j'ai toujours trouvé cet article de Robert Sosinski une excellente explication des subtilités entre blocs, procs et lambdas.
Je devrais ajouter que je pense que le message auquel je fais référence est spécifique à Ruby 1.8. Certaines choses ont changé dans Ruby 1.9, telles que les variables de bloc étant locales au bloc. En 1.8, vous obtiendrez quelque chose comme ceci:
>> a = "Hello"
=> "Hello"
>> 1.times { |a| a = "Goodbye" }
=> 1
>> a
=> "Goodbye"
Alors que 1.9 vous donnerait:
>> a = "Hello"
=> "Hello"
>> 1.times { |a| a = "Goodbye" }
=> 1
>> a
=> "Hello"
Je n'ai pas 1.9 sur cette machine, donc ce qui précède peut contenir une erreur.
Je voulais en quelque sorte ajouter pourquoi vous feriez de cette manière aux réponses déjà excellentes.
Vous ne savez pas de quelle langue vous venez, mais en supposant qu'il s'agisse d'un langage statique, ce genre de chose vous semblera familier. Voici comment lire un fichier en Java
public class FileInput {
public static void main(String[] args) {
File file = new File("C:\\MyFile.txt");
FileInputStream fis = null;
BufferedInputStream bis = null;
DataInputStream dis = null;
try {
fis = new FileInputStream(file);
// Here BufferedInputStream is added for fast reading.
bis = new BufferedInputStream(fis);
dis = new DataInputStream(bis);
// dis.available() returns 0 if the file does not have more lines.
while (dis.available() != 0) {
// this statement reads the line from the file and print it to
// the console.
System.out.println(dis.readLine());
}
// dispose all the resources after using them.
fis.close();
bis.close();
dis.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
Ignorer tout ce qui enchaîne les flux, L'idée est la suivante
C'est comme ça qu'on fait en rubis
File.open("readfile.rb", "r") do |infile|
while (line = infile.gets)
puts "#{counter}: #{line}"
counter = counter + 1
end
end
Follement différent. Briser celui-ci
Ici, au lieu de gérer les étapes 1 et 2, vous déléguez cette opération à une autre classe. Comme vous pouvez le constater, cela réduit considérablement la quantité de code que vous devez écrire, ce qui simplifie la lecture et réduit les risques de fuites de mémoire ou de verrous de fichiers non effacés.
Maintenant, ce n'est pas comme si vous ne pouviez pas faire quelque chose de similaire en Java, en fait, les gens le font depuis des décennies. C'est ce qu'on appelle le modèle Strategy . La différence est que sans blocs, pour quelque chose d'aussi simple que l'exemple de fichier, la stratégie devient excessive en raison de la quantité de classes et de méthodes que vous devez écrire. Avec les blocs, c’est une façon tellement simple et élégante de le faire que cela n’a aucun sens de NE PAS structurer votre code de cette façon.
Ce n'est pas la seule façon d'utiliser les blocs, mais les autres (comme le motif Builder, que vous pouvez voir dans l'onglet form_for d'API dans Rails) sont suffisamment similaires pour qu'il soit évident de savoir ce qui se passe une fois que vous comprenez cela. Lorsque vous voyez des blocs, il est généralement prudent de supposer que l'appel de la méthode correspond à ce que vous voulez faire et qu'il décrit comment vous voulez le faire.
J'ai trouvé cet article être très utile. En particulier, l'exemple suivant:
#!/usr/bin/Ruby
def test
yield 5
puts "You are in the method test"
yield 100
end
test {|i| puts "You are in the block #{i}"}
test do |i|
puts "You are in the block #{i}"
end
ce qui devrait donner la sortie suivante:
You are in the block 5
You are in the method test
You are in the block 100
You are in the block 5
You are in the method test
You are in the block 100
Donc, chaque fois qu'un appel est fait à yield
, Ruby exécutera le code dans le bloc do
ou à l'intérieur de {}
. Si un paramètre est fourni à yield
, il le sera en tant que paramètre du bloc do
.
Pour moi, c’était la première fois que je comprenais vraiment ce que faisaient les blocs do
. C'est en gros un moyen pour la fonction de donner accès aux structures de données internes, que ce soit pour l'itération ou pour la configuration de la fonction.
Ainsi, lorsque vous êtes dans Rails, vous écrivez ce qui suit:
respond_to do |format|
format.html { render template: "my/view", layout: 'my_layout' }
end
Ceci exécutera la fonction respond_to
qui produit le bloc do
avec le paramètre (interne) format
. Vous appelez ensuite la fonction .html
sur cette variable interne, ce qui génère à son tour le bloc de code pour exécuter la commande render
. Notez que .html
ne cédera que s’il s’agit du format de fichier demandé. (technicité: ces fonctions utilisent en réalité block.call
et non pas yield
comme vous pouvez le voir dans source mais la fonctionnalité est essentiellement la même, voir cette question pour une discussion.) Ceci fournit à la fonction un moyen de fonctionner une certaine initialisation prend alors une entrée du code appelant et continue ensuite le traitement si nécessaire.
Autrement dit, cela ressemble à une fonction prenant une fonction anonyme comme argument puis l'appelant en javascript.
En Ruby, un bloc est essentiellement un bloc de code pouvant être transmis et exécuté par n’importe quelle méthode. Les blocs sont toujours utilisés avec des méthodes, qui leur fournissent généralement des données (sous forme d'arguments).
Les blocs sont largement utilisés dans les gemmes Ruby (y compris Rails) et dans le code Ruby bien écrit. Ils ne sont pas des objets et ne peuvent donc pas être affectés à des variables.
Un bloc est un morceau de code compris entre {} ou do..end. Par convention, la syntaxe entre accolades doit être utilisée pour les blocs d'une seule ligne et la syntaxe do..end doit être utilisée pour les blocs de plusieurs lignes.
{ # This is a single line block }
do
# This is a multi-line block
end
Toute méthode peut recevoir un bloc en tant qu'argument implicite. Un bloc est exécuté par la déclaration de rendement dans une méthode. La syntaxe de base est la suivante:
def meditate
print "Today we will practice zazen"
yield # This indicates the method is expecting a block
end
# We are passing a block as an argument to the meditate method
meditate { print " for 40 minutes." }
Output:
Today we will practice zazen for 40 minutes.
Lorsque l'instruction de rendement est atteinte, la méthode meditate donne le contrôle au bloc, le code dans le bloc est exécuté et le contrôle est renvoyé à la méthode, qui reprend l'exécution immédiatement après l'instruction de rendement.
Lorsqu'une méthode contient une instruction de rendement, elle s'attend à recevoir un bloc au moment de l'appel. Si un bloc n'est pas fourni, une exception sera levée une fois que la déclaration de rendement est atteinte. Nous pouvons rendre le bloc facultatif et éviter qu'une exception ne soit déclenchée:
def meditate
puts "Today we will practice zazen."
yield if block_given?
end meditate
Output:
Today we will practice zazen.
Il n'est pas possible de transmettre plusieurs blocs à une méthode. Chaque méthode ne peut recevoir qu'un seul bloc.
Plus d’informations sur: http://www.zenruby.info/2016/04/introduction-to-blocks-in-Ruby.html
J'utilise parfois "rendement" comme ceci:
def add_to_http
"http://#{yield}"
end
puts add_to_http { "www.example.com" }
puts add_to_http { "www.victim.com"}
En termes simples, les rendements permettent à la méthode que vous créez de prendre et d'appeler des blocs. Le mot-clé de rendement est précisément l'endroit où les "éléments" du bloc seront exécutés.
Je veux faire deux remarques sur le rendement. Premièrement, bien que beaucoup de réponses évoquent différentes manières de passer d'un bloc à une méthode utilisant le rendement, parlons également du flux de contrôle. Ceci est particulièrement important car vous pouvez céder plusieurs fois à un bloc. Jetons un coup d'oeil à un exemple:
class Fruit
attr_accessor :kinds
def initialize
@kinds = %w(orange Apple pear banana)
end
def each
puts 'inside each'
3.times { yield (@kinds.tap {|kinds| puts "selecting from #{kinds}"} ).sample }
end
end
f = Fruit.new
f.each do |kind|
puts 'inside block'
end
=> inside each
=> selecting from ["orange", "Apple", "pear", "banana"]
=> inside block
=> selecting from ["orange", "Apple", "pear", "banana"]
=> inside block
=> selecting from ["orange", "Apple", "pear", "banana"]
=> inside block
Lorsque chaque méthode est invoquée, elle s'exécute ligne par ligne. Maintenant, quand nous arriverons au bloc 3.times, ce bloc sera invoqué 3 fois. Chaque fois qu'il appelle un rendement. Ce rendement est lié au bloc associé à la méthode qui a appelé chaque méthode. Il est important de noter que chaque fois que le rendement est appelé, il renvoie le contrôle au bloc de chaque méthode dans le code client. Une fois que le bloc est terminé, il revient au bloc 3.times. Et cela se produit 3 fois. Donc, ce bloc dans le code client est invoqué à 3 reprises car le rendement est appelé explicitement 3 fois.
Mon deuxième point concerne l'énumération et le rendement. enum_for instancie la classe Enumerator et cet objet Enumerator répond également au rendement.
class Fruit
def initialize
@kinds = %w(orange Apple)
end
def kinds
yield @kinds.shift
yield @kinds.shift
end
end
f = Fruit.new
enum = f.to_enum(:kinds)
enum.next
=> "orange"
enum.next
=> "Apple"
Donc, notez que chaque fois que nous appelons des types avec l’itérateur externe, il n’invoquera le rendement qu’une fois. La prochaine fois que nous l'appellerons, il invoquera le prochain rendement, etc.
Il y a une friandise intéressante en ce qui concerne enum_for. La documentation en ligne indique ce qui suit:
enum_for(method = :each, *args) → enum
Creates a new Enumerator which will enumerate by calling method on obj, passing args if any.
str = "xyz"
enum = str.enum_for(:each_byte)
enum.each { |b| puts b }
# => 120
# => 121
# => 122
Si vous ne spécifiez pas de symbole en tant qu'argument pour enum_for, Ruby connecte l'énumérateur à chaque méthode du destinataire. Certaines classes n'ont pas de méthode, comme la classe String.
str = "I like fruit"
enum = str.to_enum
enum.next
=> NoMethodError: undefined method `each' for "I like fruit":String
Ainsi, dans le cas de certains objets appelés avec enum_for, vous devez être explicite sur ce que sera votre méthode d'énumération.
Yield peut être utilisé comme bloc sans nom pour renvoyer une valeur dans la méthode. Considérons le code suivant:
Def Up(anarg)
yield(anarg)
end
Vous pouvez créer une méthode "Up" à laquelle un argument est attribué. Vous pouvez maintenant affecter cet argument à renvoyer qui appellera et exécutera un bloc associé. Vous pouvez affecter le bloc après la liste de paramètres.
Up("Here is a string"){|x| x.reverse!; puts(x)}
Lorsque la méthode Up appelle les commandes renvoyer, avec un argument, il est passé à la variable de bloc pour traiter la demande.