265 votes

Trop d'instructions "si" ?

Le code suivant fonctionne comme j'en ai besoin, mais il est laid, excessif ou un certain nombre d'autres choses. J'ai examiné les formules et tenté d'écrire quelques solutions, mais je me retrouve avec un nombre similaire d'instructions.

Existe-t-il une formule mathématique qui pourrait m'être utile dans ce cas, ou est-ce que 16 instructions "si" sont acceptables ?

Pour expliquer le code, c'est pour une sorte de jeu basé sur le tour simultané. Deux joueurs ont quatre boutons d'action chacun et les résultats viennent d'un tableau (0-3), mais les variables 'one' et 'two' peuvent être assignées à n'importe quoi si cela aide. Le résultat est, 0 = ni l'un ni l'autre ne gagne, 1 = p1 gagne, 2 = p2 gagne, 3 = les deux gagnent.

public int fightMath(int one, int two) {

    if(one == 0 && two == 0) { result = 0; }
    else if(one == 0 && two == 1) { result = 0; }
    else if(one == 0 && two == 2) { result = 1; }
    else if(one == 0 && two == 3) { result = 2; }
    else if(one == 1 && two == 0) { result = 0; }
    else if(one == 1 && two == 1) { result = 0; }
    else if(one == 1 && two == 2) { result = 2; }
    else if(one == 1 && two == 3) { result = 1; }
    else if(one == 2 && two == 0) { result = 2; }
    else if(one == 2 && two == 1) { result = 1; }
    else if(one == 2 && two == 2) { result = 3; }
    else if(one == 2 && two == 3) { result = 3; }
    else if(one == 3 && two == 0) { result = 1; }
    else if(one == 3 && two == 1) { result = 2; }
    else if(one == 3 && two == 2) { result = 3; }
    else if(one == 3 && two == 3) { result = 3; }

    return result;
}

1 votes

9 votes

Il y a sûrement une certaine logique ici qui peut être généralisée plutôt que forcée brutalement ? Il y a sûrement une fonction f(a, b) qui donne la réponse dans le cas général ? Vous n'avez pas expliqué la logique du calcul, donc toutes les réponses ne sont que du rouge à lèvres sur un cochon. Je commencerais par repenser sérieusement la logique de votre programme, en utilisant int Les drapeaux pour les actions sont très dépassés. enum peuvent contenir de la logique et sont descriptives, ce qui vous permettrait d'écrire votre code d'une manière plus moderne.

0 votes

Après avoir lu les réponses fournies par @Steve Benett dans sa question alternative liée ci-dessus, je peux supposer qu'il n'y a pas de réponse directe à cette question, car il s'agit essentiellement de la même chose qu'une base de données. J'ai essayé d'expliquer dans la question originale que je créais un jeu simple (un combattant) et que les utilisateurs avaient une sélection de 4 boutons : blockHigh(0), blockLow(1), attackHigh(2) et attackLow(3). Ces chiffres sont conservés dans un tableau jusqu'à ce qu'ils soient nécessaires. Plus tard, ils sont utilisés par la fonction 'fightMath()' qui compare les sélections de playerOne à celles de playerTwos pour obtenir le résultat. Pas de détection de collision à proprement parler.

602voto

laalto Points 50581

Si vous ne parvenez pas à trouver une formule, vous pouvez utiliser un tableau pour un nombre aussi limité de résultats :

final int[][] result = new int[][] {
  { 0, 0, 1, 2 },
  { 0, 0, 2, 1 },
  { 2, 1, 3, 3 },
  { 1, 2, 3, 3 }
};
return result[one][two];

8 votes

C'est intéressant car je n'ai jamais vu cette solution auparavant. Je ne suis pas tout à fait sûr de comprendre le résultat du retour mais je vais m'amuser à le tester.

0 votes

Le résultat de retour est la valeur du tableau à laquelle vous accéderez en utilisant un et deux comme coordonnées.

4 votes

Vous n'avez pas besoin de l'assert, Java lancera un IndexOutOfBoundsException de toute façon si un ou plusieurs indices sont hors limites.

202voto

waTeim Points 4051

Puisque votre ensemble de données est si petit, vous pouvez tout comprimer en un long entier et le transformer en une formule.

public int fightMath(int one,int two)
{
   return (int)(0xF9F66090L >> (2*(one*4 + two)))%4;
}

Plus de variante bitwise :

Cela permet d'utiliser le fait que tout est un multiple de 2.

public int fightMath(int one,int two)
{
   return (0xF9F66090 >> ((one << 3) | (two << 1))) & 0x3;
}

L'origine de la constante magique

Qu'est-ce que je peux dire ? Le monde a besoin de magie, parfois la possibilité de quelque chose appelle à sa création.

L'essence de la fonction qui résout le problème de l'OP est une carte de 2 nombres (un, deux), domaine {0,1,2,3} vers l'intervalle {0,1,2,3}. Chacune des réponses a abordé la façon d'implémenter cette carte.

De plus, vous pouvez voir dans un certain nombre de réponses une reformulation du problème sous la forme d'une carte d'un nombre à deux chiffres en base 4 N(un,deux) où un est le chiffre 1, deux est le chiffre 2, et N = 4*un + deux ; N = {0,1,2,...,15} -- seize valeurs différentes, c'est important. La sortie de la fonction est un nombre à un chiffre en base 4 {0,1,2,3} -- 4 valeurs différentes, également important.

Or, un nombre à 1 chiffre en base 4 peut être exprimé comme un nombre à 2 chiffres en base 2 ; {0,1,2,3} = {00,01,10,11}, et donc chaque sortie peut être codée avec seulement 2 bits. D'après ce qui précède, il n'y a que 16 sorties différentes possibles, donc 16*2 = 32 bits sont tout ce qui est nécessaire pour coder la carte entière ; tout cela peut tenir dans un seul entier.

La constante M est un codage de la carte m où m(0) est codé dans les bits M[0:1], m(1) est codé dans les bits M[2:3], et m(n) est codé dans les bits M[n*2:n*2+1].

Il ne reste plus qu'à indexer et retourner la partie droite de la constante, dans ce cas on peut décaler M vers la droite 2*N fois et prendre les 2 bits les moins significatifs, soit (M >> 2*N) & 0x3. Les expressions (un << 3) et (deux << 1) ne font que multiplier les choses tout en notant que 2*x = x << 1 et 8*x = x << 3.

80 votes

Intelligent, mais aucune autre personne lisant le code n'aura une chance de le comprendre.

2 votes

Pouvez-vous expliquer cela ?

109 votes

Je pense que c'est une très mauvaise pratique. Personne d'autre que l'auteur ne le comprendra. Vous voulez regarder un morceau de code et le comprendre rapidement. Mais c'est juste une perte de temps.

98voto

Eric Lippert Points 300275

Je n'aime aucune des solutions présentées, à l'exception de celle de JAB. Aucun des autres ne permet de lire facilement le code et de comprendre ce qui est calculé. .

Voici comment j'écrirais ce code - je ne connais que le C#, pas le Java, mais vous voyez le tableau :

const bool t = true;
const bool f = false;
static readonly bool[,] attackResult = {
    { f, f, t, f }, 
    { f, f, f, t },
    { f, t, t, t },
    { t, f, t, t }
};
[Flags] enum HitResult 
{ 
    Neither = 0,
    PlayerOne = 1,
    PlayerTwo = 2,
    Both = PlayerOne | PlayerTwo
}
static HitResult ResolveAttack(int one, int two)
{
    return 
        (attackResult[one, two] ? HitResult.PlayerOne : HitResult.Neither) | 
        (attackResult[two, one] ? HitResult.PlayerTwo : HitResult.Neither);
}    

Maintenant, ce qui est calculé est beaucoup plus clair : cela souligne le fait que nous calculons qui est touché par quelle attaque, et que nous retournons les deux résultats.

Cependant, cela pourrait être encore mieux ; ce tableau booléen est quelque peu opaque. J'aime l'approche de la consultation du tableau, mais je serais enclin à l'écrire de manière à ce que la sémantique du jeu soit claire. C'est à dire, plutôt que "une attaque de zéro et une défense de un ne donne aucun résultat", trouvez plutôt un moyen de faire en sorte que le code implique plus clairement "une attaque de coup de pied bas et une défense de bloc bas ne donne aucun résultat". Faites en sorte que le code reflète la logique commerciale du jeu.

67 votes

C'est absurde. La plupart des programmes légèrement expérimentés seront capables d'apprécier les conseils donnés ici et d'appliquer le style de codage à leur propre langage. La question était de savoir comment éviter une série de "si". Ceci montre comment.

6 votes

@user3414693 : Je suis bien conscient que c'est une question Java. Si vous lisez attentivement la réponse, cela devient clair. Si vous pensez que ma réponse n'est pas judicieuse, je vous encourage à écrire votre propre réponse que vous préférez.

1 votes

@EricLippert J'aime aussi la solution de JAB. IMHO, le type enum en C# laisse beaucoup à désirer. Il ne suit pas la philosophie de la fosse du succès que le reste des fonctionnalités font. Par exemple stackoverflow.com/a/847353/92414 L'équipe c# a-t-elle prévu de créer un nouveau type d'enum (afin d'éviter de casser le code existant) qui soit mieux conçu ?

87voto

djjolem Points 1138

Vous pouvez créer une matrice qui contient les résultats

int[][] results = {{0, 0, 1, 2}, {0, 0, 2, 1},{2, 1, 3, 3},{2, 1, 3, 3}};

Lorsque vous souhaitez obtenir une valeur, vous utiliserez

public int fightMath(int one, int two) {
  return this.results[one][two]; 
}

70voto

JAB Points 11053

D'autres personnes ont déjà suggéré mon idée initiale, la méthode matricielle, mais en plus de consolider les instructions if, vous pouvez éviter une partie de ce que vous avez en vous assurant que les arguments fournis sont dans la plage attendue et en utilisant des retours en place (certaines normes de codage que j'ai vues imposent un point de sortie pour les fonctions, mais j'ai trouvé que les retours multiples sont très utiles pour éviter le codage en flèche et avec la prévalence des exceptions en Java, il n'y a pas beaucoup d'intérêt à appliquer strictement une telle règle de toute façon, car toute exception non attrapée lancée à l'intérieur de la méthode est un point de sortie possible). L'imbrication d'instructions switch est une possibilité, mais pour la petite gamme de valeurs que vous vérifiez ici, je trouve que les instructions if sont plus compactes et ne sont pas susceptibles d'entraîner une grande différence de performances, en particulier si votre programme est basé sur des tours plutôt que sur du temps réel.

public int fightMath(int one, int two) {
    if (one > 3 || one < 0 || two > 3 || two < 0) {
        throw new IllegalArgumentException("Result is undefined for arguments outside the range [0, 3]");
    }

    if (one <= 1) {
        if (two <= 1) return 0;
        if (two - one == 2) return 1;
        return 2; // two can only be 3 here, no need for an explicit conditional
    }

    // one >= 2
    if (two >= 2) return 3;
    if (two == 1) return 1;
    return 2; // two can only be 0 here
}

Cela finit par être moins lisible qu'il ne le serait autrement en raison de l'irrégularité de certaines parties de la correspondance entrée->résultat. Je préfère le style matriciel en raison de sa simplicité et de la façon dont vous pouvez configurer la matrice pour qu'elle ait un sens visuellement (bien que cela soit en partie influencé par mes souvenirs des cartes de Karnaugh) :

int[][] results = {{0, 0, 1, 2},
                   {0, 0, 2, 1},
                   {2, 1, 3, 3},
                   {2, 1, 3, 3}};

Mise à jour : étant donné que vous avez mentionné le blocage/le frappement, voici une modification plus radicale de la fonction qui utilise des types énumérés propriétaires/attributs pour les entrées et le résultat et qui modifie aussi un peu le résultat pour tenir compte du blocage, ce qui devrait donner une fonction plus lisible.

enum MoveType {
    ATTACK,
    BLOCK;
}

enum MoveHeight {
    HIGH,
    LOW;
}

enum Move {
    // Enum members can have properties/attributes/data members of their own
    ATTACK_HIGH(MoveType.ATTACK, MoveHeight.HIGH),
    ATTACK_LOW(MoveType.ATTACK, MoveHeight.LOW),
    BLOCK_HIGH(MoveType.BLOCK, MoveHeight.HIGH),
    BLOCK_LOW(MoveType.BLOCK, MoveHeight.LOW);

    public final MoveType type;
    public final MoveHeight height;

    private Move(MoveType type, MoveHeight height) {
        this.type = type;
        this.height = height;
    }

    /** Makes the attack checks later on simpler. */
    public boolean isAttack() {
        return this.type == MoveType.ATTACK;
    }
}

enum LandedHit {
    NEITHER,
    PLAYER_ONE,
    PLAYER_TWO,
    BOTH;
}

LandedHit fightMath(Move one, Move two) {
    // One is an attack, the other is a block
    if (one.type != two.type) {
        // attack at some height gets blocked by block at same height
        if (one.height == two.height) return LandedHit.NEITHER;

        // Either player 1 attacked or player 2 attacked; whoever did
        // lands a hit
        if (one.isAttack()) return LandedHit.PLAYER_ONE;
        return LandedHit.PLAYER_TWO;
    }

    // both attack
    if (one.isAttack()) return LandedHit.BOTH;

    // both block
    return LandedHit.NEITHER;
}

Il n'est même pas nécessaire de modifier la fonction elle-même si vous voulez ajouter des blocs/attaques de plus grande hauteur, juste les enums ; l'ajout de types de mouvements supplémentaires nécessitera probablement une modification de la fonction, cependant. Aussi, EnumSet s pourrait être plus extensible que d'utiliser des enums supplémentaires comme propriétés de l'enum principal, par exemple EnumSet<Move> attacks = EnumSet.of(Move.ATTACK_HIGH, Move.ATTACK_LOW, ...); et ensuite attacks.contains(move) plutôt que move.type == MoveType.ATTACK mais en utilisant EnumSet seront probablement légèrement plus lentes que les chèques d'égalité directs.


Pour le cas où un bloc réussi donne lieu à un compteur, vous pouvez remplacer if (one.height == two.height) return LandedHit.NEITHER; con

if (one.height == two.height) {
    // Successful block results in a counter against the attacker
    if (one.isAttack()) return LandedHit.PLAYER_TWO;
    return LandedHit.PLAYER_ONE;
}

En outre, le remplacement de certains des if avec l'utilisation de l'opérateur ternaire ( boolean_expression ? result_if_true : result_if_false ) pourrait rendre le code plus compact (par exemple, le code du bloc précédent deviendrait return one.isAttack() ? LandedHit.PLAYER_TWO : LandedHit.PLAYER_ONE; ), mais cela peut conduire à des oneliners plus difficiles à lire et je ne le recommande pas pour des branchements plus complexes.

0 votes

Je vais certainement me pencher sur cette question, mais mon code actuel me permet d'utiliser la valeur int de l'option one y two pour être réutilisés comme points de départ sur ma feuille de sprites. Bien que cela ne nécessite pas beaucoup de code supplémentaire pour le permettre.

2 votes

@TomFirth84 Il y a un EnumMap que vous pouvez utiliser pour faire correspondre les enums à vos décalages entiers (vous pouvez également utiliser les valeurs ordinales des membres de l'enum directement, par ex. Move.ATTACK_HIGH.ordinal() serait 0 , Move.ATTACK_LOW.ordinal() serait 1 etc., mais c'est plus fragile/moins flexible que d'associer explicitement chaque membre à une valeur, car l'ajout de valeurs d'énumération entre celles qui existent déjà perturberait le comptage, ce qui ne serait pas le cas avec un fichier EnumMap .)

7 votes

Il s'agit de la solution la plus lisible, car elle traduit le code en quelque chose qui a du sens pour la personne qui le lit.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X