Si un codeur écrit du code que personne d'autre qu'il ne peut comprendre et que les révisions de code se terminent toujours par le critique se grattant la tête ou tenant la tête dans leurs mains, est-ce un signe clair que le codeur n'est tout simplement pas conçu pour une programmation professionnelle? Serait-ce suffisant pour justifier un changement de carrière? Quelle est l'importance du code compréhensible dans cette industrie?
Considérez ces exemples C, comparez:
if (b)
return 42;
else
return 7;
contre:
return (b * 42) | (~(b - 1) * 7);
Y a-t-il jamais une excuse pour utiliser ce dernier exemple? si oui, quand et pourquoi?
EDIT: Laisser le dernier extrait original pour examen, en ajoutant une correction:
return (b * 42) | ((b - 1) & 7);
Je pense que l'autre observation intéressante est qu'elle nécessite que b soit 1 pour vrai et 0 pour faux, toute autre valeur rendrait des résultats étranges.
La première règle de tout ingénieur logiciel professionnel est d'écrire du code compréhensible. Le deuxième exemple ressemble à un exemple optimisé pour un compilateur plus ancien et non optimisant ou simplement quelqu'un qui souhaite s'exprimer avec des opérateurs au niveau du bit. Il est assez clair ce qui se passe si nous connaissons les opérations au niveau du bit, mais à moins que vous ne soyez dans un domaine où c'est le cas, évitez le deuxième exemple. Je dois également souligner que les accolades sont manquantes dans le premier exemple.
Le codeur peut insister sur le fait que son code est efficace, cela peut être le cas, mais s'il ne peut pas être maintenu, il pourrait tout aussi bien ne pas être écrit du tout, car il générera une dette technique horrible en cours de route.
Il y a plusieurs questions que vous soulevez.
1) Est-ce un signe clair que le codeur n'est pas conçu pour une programmation professionnelle?
2) Serait-ce suffisant pour justifier un changement de carrière?
3) Quelle est l'importance du code compréhensible dans cette industrie?
4a) Les exemples C:
4b) Y a-t-il jamais une excuse pour utiliser ce dernier exemple?
Oui. De nombreux processeurs utilisent la prévision des succursales et des pipelines. Le coût d'une succursale incorrectement prévue peut entraîner des retards inacceptables. Pour obtenir une réponse plus déterministe, le twiddling de bits peut être utilisé pour lisser les choses.
Mon propre point de vue sur le 2ème exemple est qu'il est inutilement compliqué et devrait être retravaillé pour plus de clarté. De plus, je n'aime pas la multiplication car (selon l'architecture) elle peut être lente par rapport à d'autres opérations. Très probablement, je préférerais voir quelque chose de la forme:
return b ? 42 : 7;
Cependant, selon les circonstances (ET s'il pouvait être démontré qu'il fournissait de meilleurs résultats sensiblement meilleurs dans les catégories critiques), I pourrait accepter une macro avec un nom descriptif approprié lors de la révision du code. Par exemple:
/*
* APPROPRIATE_MACRO_NAME - use (x) if (condition) is 1, (y) if (condition) is 0
*
* Parameter Restrictions:
* (condition) - must be either 1 or 0
* (x) and (y) are of the same integer type
*
* This macro generates code that avoids branching (and thus incorrectly
* predicted branches). Its use should be restricted to <such and such>
* because <why and why>.
*/
#define APPROPRIATE_MACRO_NAME(condition,x,y) \
(((condition - 1) & ((y) - (x))) + (x))
J'espère que cela t'aides.
Le deuxième code ne renvoie pas 42 ou 7.
for b = 1:
(1 * 42) | (~(1 - 1) * 7)
42 | (~(0) * 7)
42 | (-1 * 7)
42 | -7
-5
for b = 0:
(0 * 42) | (~(0 - 1) * 7)
0 | (~(-1) * 7)
0 | (0 * 7)
0 | 0
0
Et pourtant, lorsque vous l'avez publié, vous pensiez que c'était le cas, c'est pourquoi vous devriez éviter le code alambiqué.
Cependant, prenez un code "correct" par exemple comme:
return ((b - 1) & (7 ^ 42)) ^ 42;
Il y a deux raisons pour lesquelles je peux penser que cela peut être utile. - L'architecture pour laquelle vous écrivez ne prend pas en charge les instructions de branchement ou prédites. - L'architecture pour laquelle vous écrivez a un pipeline qui ne fonctionne pas après une opération de branche ou a un coût prohibitif associé à une erreur de prédiction de branche.
Dans ce cas, vous écririez quelque chose dans ce sens:
/*
This code is equivalent to:
if (b)
return 42;
else
return 7;
when b=0 or b=1
But does not include a branch instruction since a branch prediction
miss here would cause an unacceptable impact to performance.
*/
return ((b - 1) & (7 ^ 42)) ^ 42;
Si toutefois vous ne voyez qu'un code comme celui-ci sans explication ni justification, c'est probablement un signe d'obscurcissement du code source. L'obfuscation du code source (par opposition à l'obfuscation du code binaire, que beaucoup considéreraient comme ayant des objectifs légitimes) a tendance à être néfaste. Certains programmeurs peuvent être territoriaux pour diverses raisons, parfois pour la sécurité de l'emploi et parfois pour un plus grand contrôle sur ce qui est fait et comment les choses sont faites à cause de l'ego, de l'insécurité ou de la méfiance. Cependant, c'est presque toujours contre les intérêts de ses pairs et de son employeur. Il est de la responsabilité de celui qui est en charge de l'équipe de mettre un terme à un tel comportement le plus tôt possible, que ce soit en instaurant une confiance mutuelle ou en instillant la peur, selon le style de gestion du leader et les méthodes auxquelles le programmeur individuel réagit.
Il y a de fortes chances que quelqu'un écrivant (b * 42) | (~(b - 1) * 7)
Soit soit quelqu'un qui en sait très peu sur la programmation essayant de prétendre être expérimenté/bien informé/etc, soit quelqu'un qui essaie de saboter un projet (c'est-à-dire qu'il est trop expérimenté/bien informé/intelligent et veulent la sécurité d'emploi).
Le premier type de personne veut montrer qu'elle sait comment utiliser NOT, OR, l'ordre des opérations, etc. Elle montre ses connaissances, mais, hélas, elle écrit du code beaucoup moins efficace, car cela nécessite deux multiplications , une soustraction, un pas et un ou, ce qui est clairement moins efficace qu'une comparaison, un couple saute et un retour.
Si tel est le cas, ils ne méritent pas (encore) d'être dans l'industrie, mais leur démonstration prouve qu'ils connaissent la logique informatique de base et pourraient être une ressource précieuse un jour, une fois qu'ils auront passé le showboating et commencer à écrire du code efficace. En outre, il existe une possibilité distincte que b ne soit pas nécessairement 0 ou 1, ce qui entraînerait le retour d'une valeur complètement différente. Cela pourrait introduire des bogues subtils qui peuvent être difficiles à trouver.
Le deuxième type de personne espère glisser du code comme celui-ci (ou de nombreux autres types de code sournois), afin que les gens continuent à leur poser des questions sur le code, de sorte qu'ils sont jugés trop précieux pour être perdus. Ce type de sabotage finira par nuire à un projet, et ils devraient être lâchés immédiatement jusqu'à ce qu'ils apprennent leur leçon et écrivent du code optimisé et facile à lire. Il y a aussi la possibilité que b ne soit pas 1 ou 0, comme auparavant, ce qui signifie qu'il retournera une valeur différente de celle attendue (42 ou 7), ce qui peut fonctionner correctement jusqu'à ce qu'un programmeur malchanceux le revienne à if(b) ... else ...
et le code cesse soudainement de fonctionner. Par exemple, il s'agit peut-être d'un générateur de pseudo-nombres déguisé en une instruction if très coûteuse.
Le code lisible est important, ainsi que le code libre (autant que pratique) de bogues logiques comme celui-ci. Quiconque a sérieusement écrit du code pendant un certain temps sait à quel point c'est important. Écrivez un jeu entièrement fonctionnel de Tic Tac Toe. Maintenant, mettez ce code de côté pendant un an, puis revenez-y et essayez de lire le code. Si vous l'avez écrit de manière désinvolte, sans égard aux normes de codage, aux commentaires, à la documentation, etc., vous ne reconnaîtriez probablement même pas que le code que vous avez écrit a été tapé par vous, encore moins comment le corriger ou ajouter une nouvelle fonctionnalité si quelque chose était cassé ou devait être mis à jour.
Lorsque plusieurs développeurs travaillent ensemble, il est encore plus important que le code soit lisible, car il y a de fortes chances que vous ne conserviez pas ce code. Vous serez passé à autre chose et quelqu'un d'autre devra maintenir votre code. Inversement, vous pouvez hériter d'un autre projet et vous espérez que les développeurs avant vous ont laissé des commentaires et du code propre pour que vous puissiez travailler. Les programmeurs travaillant sur du code fiable écrivent le code pour qu'il soit maintenable, y compris la lisibilité et les commentaires.
Les performances, bien qu'importantes, ne devraient pas l'emporter sur la lisibilité. Au minimum, si quelqu'un a utilisé le deuxième bloc de code ici, je m'attendrais à un long bloc de commentaires qui explique clairement pourquoi il l'a fait de cette façon au lieu d'une manière plus conventionnelle. À ce stade, je déciderais probablement de le remplacer par un code plus conventionnel s'il n'y avait aucune bonne raison à cela. Si, en effet, c'était une bombe logique, je la réécrirais plus longtemps afin qu'il soit compris que la fonction doit être de cette façon pour éviter les bugs, ainsi qu'une documentation claire de ce qu'elle fait vraiment.
En fin de compte, je suis presque sûr qu'il existe une utilisation légitime pour un problème spécialisé qui, par hasard, a besoin de cet algorithme précis. Si c'est le cas, cependant, je m'attendrais à des commentaires expliquant l'algorithme qui utilise cette logique, et il vaut mieux que ce soit pour quelque chose de mieux qu'un bloc if-else. Les deux seuls blocs if-else appropriés pour l'exemple spécifique sont: if(b) return 42; return 7;
(else est facultatif) et return b? 42: 7;
(Les opérateurs ternaires conviennent à la logique des petites branches, sauf si les normes de codage l'interdisent entièrement) . Tout le reste est trop compliqué et devrait être réduit d'une déclaration plus simple.
Je me retrouve parfois à écrire du code "délicat" que les développeurs plus jeunes peuvent ne pas comprendre immédiatement, et je commente généralement ce code afin qu'ils puissent comprendre pourquoi il a été écrit tel qu'il était. Parfois, le code optimisé peut être difficile à lire et pourtant nécessaire, mais cela devrait être l'exception plutôt que la règle.
Par coïncidence, il existe un endroit parfaitement acceptable pour un code comme celui-ci. Concours d'obscurcissement. Dans ce cas, je réserverais un jugement pour la fonction jusqu'à ce que je détermine que le code était juste une branche vraiment intelligente, gaspillant le CPU, ou s'il s'agissait d'un appareil plus sournois pour générer des nombres pseudo-aléatoires, la valeur de PI (ou un autre nombre magique), ou peut-être même un algorithme de cryptage faible.
Si les branches dans if
/then
/else
sont un problème, alors il est probablement plus facile de passer simplement à quelque chose comme:
static const int values[] = {6, 42};
return values[b!=0];
Cela fonctionne réellement et bien que certains puissent le trouver légèrement moins lisible que le if
/then
/else
, cela ne devrait certainement pas être une obstruction perceptible à quiconque connaît C ou C++ du tout.
Pour tous ceux qui devraient penser que c'est une sale astuce, ou dépend d'une vérification de type particulièrement lâche dans une langue particulière: oui et non. Tel qu'il est écrit, le code dépend de la conversion de "faux" en 0 et de "vrai" en 1 en C et C++.
L'idée de base, cependant, peut être appliquée aussi bien dans d'autres langues, y compris celles qui ont des systèmes de type sensiblement plus serrés. Par exemple, en Pascal, la même idée de base ressemblerait à:
var
values : array [boolean] of Integer;
(* ... *)
values[false] = 6;
values[true] = 42;
(* ... *)
f = values[b<>0];
Le Manuel d'utilisation et rapport Pascal (2dakota du Nord Edition) de Kathleen Jensen et Niklaus Wirth montre l'utilisation d'un booléen comme index dans un tableau à plusieurs endroits, tels que §6.2.1 et §7. Bien que Pascal (tel que défini à l'origine) n'inclut pas la notion de variables initialisées, si tel était le cas, les initialiseurs finiraient dans le même ordre qu'en C et C++ (car il définit le fait que false < true
).
Ada utilise une syntaxe légèrement différente, mais offre les mêmes capacités de base:
Temp : array(Boolean) of Integer := (false => 7, true=>42);
-- ...
Return_Value = Temp( 0 /= i);
Donc non, nous ne traitons pas avec quelque chose qui n'est qu'un accident d'une langue qui utilise une vérification de type particulièrement lâche. Pascal et (en particulier) Ada sont bien connus pour être particulièrement fortement typés, mais tous deux prennent en charge la même construction de base que C et C++, et le font essentiellement de la même manière.
Quelque chose comme ce qui suit contribuerait à rendre plus visible l'intention du code:
manifoldPressureFloor = (b * 42) | (~(b - 1) * 7);
return manifoldPressureFloor;
manifoldPressureFloor
est totalement composé, bien sûr, je n'ai aucune idée de ce que le code original est en fait.
Mais sans une quelconque explication ou justification du code obscurci, le réviseur de code et/ou le programmeur de maintenance est en position d'avoir aucune idée claire de ce que le programmeur original voulait vraiment accomplir, ce qui rend à son tour il est presque impossible de prouver que le code fonctionne réellement (qu'il fait réellement ce qu'il est censé faire). Et cela rend la programmation de maintenance à la fois douloureuse et beaucoup plus susceptible d'introduire des bogues.
Je ne crois pas (sans qualifications) que le code puisse être complètement auto-commenté. Toutefois; Je suis totalement convaincu qu'il est possible de pencher fortement dans cette direction.
S'il y a une raison inhabituelle (ou Edge, performance ou autre non spécifiée jusqu'à présent) pour laquelle ce (b * 42) | (~(b - 1) * 7)
la syntaxe est justifiée, le réviseur de code n'a pas été en mesure de saisir facilement ce fait parce que L'INTENTION du code n'est pas facilement déchiffrable, et il n'y a apparemment aucun commentaire expliquant pourquoi cela a été fait.
Un code intelligent juste pour être intelligent devrait être évité. L'un des principaux objectifs de l'écriture d'un code clair est d'assurer la compréhension humaine et d'empêcher de manière proactive l'introduction de bogues à l'avenir. Un code intelligent qui n'est pas clairement compréhensible est une bombe à retardement. Même si cela fonctionne aujourd'hui, il va cesser de fonctionner après la maintenance du code demain, ou dans un an. Et les bogues seront ésotériques et difficiles à trouver et à corriger.
Comment quelqu'un d'autre que le programmeur d'origine peut-il prouver que le code fonctionne si l'intention et la syntaxe ne sont pas claires? Comment même le programmeur d'origine peut-il prouver que le code fonctionne dans ce cas?
Les commentaires doivent être requis dans le code de cas Edge vissé et intelligent. Mais c'est mieux si le code est assez simple pour ne pas exiger les commentaires. Ces commentaires finissent également par être obsolètes ou non pertinents si le code est mis à jour et que le programmeur de maintenance ne met pas à jour ou ne supprime pas les commentaires. Les programmeurs mettent régulièrement à jour le code sans mettre à jour les commentaires. Ils ne devraient pas , ils le font .
Tous ces facteurs conduisent à la conclusion que ce programmeur doit résoudre les problèmes de pouvoir prouver que le code fonctionne, rendre le code compréhensible par d'autres êtres humains et rendre le code robuste. Le code très sensible à l'introduction de nouveaux bogues lors de la maintenance est fragile. Aucun employeur ne veut payer beaucoup d'argent pour le développement d'un code fragile et sujet aux bogues.
Un code fragile, dont le comportement est indémontrable, est beaucoup plus cher qu'un code propre, clair, prouvable (testable), facile à comprendre et facile à entretenir. Le programmeur est payé par quelqu'un d'autre pour être un professionnel et produire un produit raisonnablement propre, robuste et maintenable à un coût raisonnable. Droite?
Donc en conclusion, le programmeur qui a trouvé ce code est évidemment intelligent, et donc plein de potentiel. Mais les problèmes de pouvoir prouver l'exactitude du code, rendent le code robuste et compréhensible doivent être pris au sérieux. Si ce changement peut avoir lieu, le programmeur vaut probablement la peine de continuer. Sinon, et le programmeur insiste pour continuer à faire les choses de cette façon, il pourrait devenir difficile de justifier de les garder dans l'équipe, car ils vous coûteront certainement plus cher qu'ils ne vous feront à long terme. Et c'est le vrai problème, non?
A MON HUMBLE AVIS.
Si on me présentait du code comme celui-ci dans une revue de code, j'aurais deux questions à poser:
Pourquoi avons-nous choisi de l'écrire de cette façon? Étant donné que la manipulation de bits comme celle-ci est utilisée pour contourner une sorte de goulot d'étranglement des performances, on pourrait supposer que nous avons un goulot d'étranglement qui est corrigé si nous employons cette approche à la place. Une question de suivi immédiate serait: "Comment avons-nous prouvé qu'il s'agissait d'un goulot d'étranglement critique?"
Avons-nous une sorte de cadre d'acceptation (tests unitaires, etc.) qui prouve que l'approche optimisée est équivalente à l'approche non optimisée? Si nous ne le faisons pas, c'est à ce moment-là que les alarmes sonnent, mais elles ne sont pas aussi fortes qu'on pourrait le penser.
En fin de compte, le code que vous écrivez devrait être maintenable . Un code rapide mais difficile à aborder lorsqu'un bogue se produit n'est pas un bon code. S'ils pouvaient répondre à mes deux préoccupations ci-dessus, je laisserais probablement tomber une demande d'ajout à la fois de la justification et de sa forme équivalente non binaire.
En général, une révision du code devrait identifier et clarifier ces lacunes; s'il n'y avait aucune raison justifiable d'écrire le code de cette façon, ou que le code n'est tout simplement pas correct (comme quelques autres l'ont souligné ici), il n'y a aucune raison de l'avoir écrit de cette façon en premier lieu, et il doit être corrigé . S'il s'agit d'une tendance continue; c'est-à-dire qu'un développeur continue d'écrire du code qui est efficace mais horriblement obscurci, à ce stade, son responsable ou sa direction devrait intervenir et réitérer l'importance d'un code clair.
Le remplacement d'if-s par une expression arithmétique/logique est parfois nécessaire lorsqu'un long processus de processeur est requis. Cela fait que le code exécute toujours les mêmes instructions quelle que soit la condition, ce qui le rend plus adapté à la parallélisation.
Cela dit, l'échantillon fourni est erroné, car les deux échantillons ne sont pas équivalents:
if (b)
return 42;
else
return 7;
peut être return ((b&1)*42)|((1-(b&1)))*7
Le programmeur de l'exemple OP a peut-être fait une élimination if incorrecte, ou l'OP lui-même peut avoir mal interprété les intentions du programmeur lorsqu'il a deviné le commutateur traditionnel if
de manière incorrecte. Il est difficile de dire laquelle des deux est correcte sans savoir quelle était l'exigence.
Notez que l'expression n'est pas telle que "obscure": c'est juste une combinaison linéaire sous la forme P*(t)+Q*(1-t)
(avec t = 0..1) dont tout programmeur doit être conscient, car c'est la base de la plupart de l'algèbre linéaire.
La seule chose que je peux comprendre, c'est qu'entre le codeur et le réviseur, il existe une base culturelle différente à propos du traitement parallèle et de l'algèbre linéaire. Cette IMHO, ne fait pas l'un supérieur à l'autre, si l'exactitude est prouvée.
Outre les excellents points soulevés par Sparky (points 1 et 3) et Donscarletti, ce message m'amène à un autre auquel j'ai répondu il y a longtemps: Comment puis-je documenter mon code? .
Beaucoup de gens sont ou se disent programmeurs, certains sont bons, peu sont excellents. Tout comme dans beaucoup d'autres horizons. Vous pouvez décider de juger ceux qui semblent moins bons que vous ou moins bons que ce que vous attendez d'eux (pas très utile, perte de temps), vous pouvez essayer de les aider (excellents) ou les contraindre à faire mieux ( vous n'avez peut-être pas le choix), ou ignorez-les et faites simplement de votre mieux (vous n'avez peut-être pas le choix, c'est parfois le meilleur itinéraire). Quel que soit votre choix d'action, cela dépend généralement de nombreux facteurs bien au-delà de la simple capacité technique.