Czy przypisywanie w warunku jest częścią złej praktyki?


35

Załóżmy, że chcę napisać funkcję, która łączy dwa ciągi znaków w C. Sposób, w jaki napisałbym to:

void concat(char s[], char t[]){
    int i = 0;
    int j = 0;

    while (s[i] != '\0'){
        i++;
    }

    while (t[j] != '\0'){
        s[i] = t[j];
        i++;
        j++;
    }

    s[i] = '\0';

}

Jednak K&R w swojej książce zaimplementował to inaczej, szczególnie włączając w to możliwie jak najwięcej części warunkowej pętli while:

void concat(char s[], char t[]){
    int i, j;
    i = j = 0;
    while (s[i] != '\0') i++;

    while ((s[i++]=t[j++]) != '\0');

}

Który sposób jest preferowany? Czy zachęca się lub zniechęca do pisania kodu tak, jak robi to K&R? Wierzę, że moja wersja byłaby łatwiejsza do odczytania przez innych ludzi.


38
Nie zapominaj, że K&R został opublikowany po raz pierwszy w 1978 roku. Od tego czasu wprowadziliśmy kilka drobnych zmian w sposobie kodowania.
corsiKa

28
Czytelność była bardzo różna w czasach teleprinterów i redaktorów zorientowanych na linię. Umieszczanie tego wszystkiego w jednym wierszu było kiedyś bardziej czytelne.
user2357112 obsługuje Monikę

15
Jestem zszokowany, że mają indeksy i porównania do „\ 0” zamiast czegoś w stylu while (*s++ = *t++); (Moje C jest bardzo zardzewiałe, czy potrzebuję tam parens, aby operator miał pierwszeństwo?) Czy K&R wydało nową wersję swojej książki? Ich oryginalna książka miała wyjątkowo zwięzły i idiomatyczny kod.
user949300 30.10.16

4
Czytelność jest bardzo osobistą sprawą - nawet w czasach teletypów. Różni ludzie wolą różne style. Wiele wkuwania instrukcji dotyczyło generowania kodu. W tamtych czasach niektóre zestawy instrukcji (np. Dane ogólne) mogą łączyć kilka operacji w jedną instrukcję. Ponadto na początku lat 80. istniał mit, że stosowanie nawiasów generuje więcej instrukcji. Musiałem wygenerować asembler, aby udowodnić recenzentowi kodu, że to mit.
puchar

10
Zauważ, że dwa bloki kodu nie są równoważne. Pierwszy blok kodu nie skopiuje Kończący '\0'z t(na whilewyjściach pierwszy). Spowoduje to pozostawienie powstałego sciągu bez zakończenia '\0'(chyba że lokalizacja pamięci została już wyzerowana). Drugi blok kodu utworzy kopię zakończenia '\0'przed wyjściem z whilepętli.
Makyen

Odpowiedzi:


80

Zawsze wolę jasność niż spryt. W przeszłości najlepszym programistą był ten, którego kodu nikt nie mógł zrozumieć. „Nie rozumiem jego kodu, musi być geniuszem” - powiedzieli. Obecnie najlepszym programistą jest ten, którego kod każdy może zrozumieć. Czas na komputerze jest teraz tańszy niż czas programisty.

Każdy głupiec może napisać kod zrozumiały dla komputera. Dobrzy programiści piszą kod, który ludzie mogą zrozumieć. (M. Fowler)

Bez wątpienia wybrałbym opcję A. I to jest moja ostateczna odpowiedź.


8
Bardzo treściwa ideologia, ale faktem jest, że nie ma nic złego w przypisaniu warunkowym. Zdecydowanie lepiej jest wcześniejsze wyjście z pętli lub powielenie kodu przed i wewnątrz pętli.
Miles Rout

26
@MilesRout Jest. Coś jest nie tak z jakimkolwiek kodem, który wywołuje efekt uboczny tam, gdzie się go nie spodziewasz, np. Przekazując argumenty funkcji lub oceniając warunki warunkowe. Nie wspominając nawet o tym, że if (a=b)można go łatwo pomylić if (a==b).
Arthur Havlicek

12
@Luke: „Moje IDE może oczyścić X, dlatego nie stanowi problemu” jest raczej nieprzekonujące. Jeśli to nie problem, dlaczego IDE sprawia, że ​​tak łatwo jest to naprawić?
Kevin

6
@ArthurHavlicek Zgadzam się z twoim ogólnym punktem, ale kod z efektami ubocznymi w warunkach warunkowych nie jest tak naprawdę niezwykły: while ((c = fgetc(file)) != EOF)jako pierwszy, jaki przychodzi mi do głowy.
Daniel Jour

3
+1 „Biorąc pod uwagę, że debugowanie jest przede wszystkim dwa razy trudniejsze niż pisanie programu, jeśli jesteś tak sprytny, jak tylko potrafisz, pisząc go, to jak go kiedykolwiek debugować?” BWKernighan
Christophe

32

Złota zasada, podobnie jak w odpowiedzi Tulainsa Córdovej, polega na pisaniu zrozumiałego kodu. Ale nie zgadzam się z wnioskiem. Ta złota zasada oznacza pisanie kodu, który zrozumie typowy programista, który skończy utrzymywanie kodu. I jesteś najlepszym sędzią, kto jest typowym programistą, który skończy utrzymywać twój kod.

Dla programistów, którzy nie zaczynali od C, pierwsza wersja jest prawdopodobnie łatwiejsza do zrozumienia, z powodów, które już znasz.

Dla tych, którzy dorastali w tym stylu C, druga wersja może być łatwiejsza do zrozumienia: dla nich równie zrozumiałe jest to, co robi kod, dla nich pozostawia mniej pytań, dlaczego jest napisane tak, jak jest, i dla nich , mniej miejsca w pionie oznacza, że ​​na ekranie można wyświetlić więcej kontekstu.

Musisz polegać na własnym rozsądku. Dla kogo chcesz ułatwić zrozumienie kodu? Czy ten kod jest napisany dla firmy? Zatem docelowymi odbiorcami są prawdopodobnie inni programiści w tej firmie. Czy to osobisty projekt hobby, nad którym nikt nie będzie pracował, tylko Ty? Jesteś więc własną grupą docelową. Czy ten kod chcesz udostępnić innym? Ci inni to twoi odbiorcy docelowi. Wybierz wersję pasującą do tej grupy odbiorców. Niestety nie ma jednego preferowanego sposobu zachęcania.


14

EDYCJA: Linia s[i] = '\0';została dodana do pierwszej wersji, tym samym naprawiając ją zgodnie z opisem w wariancie 1 poniżej, więc nie dotyczy to już bieżącej wersji kodu pytania.

Druga wersja ma tę wyraźną zaletę, że jest poprawna , podczas gdy pierwsza nie jest - nie kończy poprawnie łańcucha docelowego.

„Przypisanie w stanie” pozwala bardzo zwięźle wyrazić koncepcję „kopiuj każdy znak przed sprawdzeniem znaku zerowego” oraz w sposób, który sprawia, że ​​optymalizacja kompilatora jest nieco łatwiejsza, chociaż wielu inżynierów oprogramowania uważa, że ​​ten styl kodu jest mniej czytelny . Jeśli nalegasz na użycie pierwszej wersji, będziesz musiał albo

  1. dodaj zakończenie zerowe po zakończeniu drugiej pętli (dodając więcej kodu, ale możesz argumentować, że readabiliy sprawia, że ​​jest to opłacalne) lub
  2. zmień treść pętli na „najpierw przypisuj, następnie sprawdź lub zapisz przypisany znak, a następnie zwiększ indeksy”. Sprawdzenie stanu na środku pętli oznacza wyrwanie się z pętli (zmniejszenie klarowności, na co patrzy większość zmarłych). Zapisanie przypisanego znaku oznaczałoby wprowadzenie zmiennej tymczasowej (zmniejszenie przejrzystości i wydajności). Oba z nich zniweczyłyby moim zdaniem przewagę.

Poprawne jest lepsze niż czytelne i zwięzłe.
user949300,

5

Odpowiedzi Tulainsa Córdova i hvd dość dobrze obejmują aspekty przejrzystości / czytelności. Pozwól mi rzucić okiem jako kolejny powód na korzyść zadań w warunkach. Zmienna zadeklarowana w warunku jest dostępna tylko w zakresie tej instrukcji. Nie możesz później użyć tej zmiennej przypadkowo. Do pętli robi to od wieków. I jest to na tyle ważne, że nadchodzące C ++ 17 wprowadzi podobną składnię dla if i switch :

if (int foo = bar(); foo > 42) {
    do_stuff();
}

foo = 23;   // compiler error: foo is not in scope

3

Nie. Jest to bardzo standardowy i normalny styl C. Twój przykład jest zły, ponieważ powinien być po prostu pętlą for, ale ogólnie nie ma w tym nic złego

if ((a = f()) != NULL)
    ...

na przykład (lub z chwilą).


7
Coś jest z tym nie tak; `! = NULL` i jego krewni w warunku C są natter, tylko po to, aby uspokoić programistów, którzy nie są zadowoleni z koncepcji wartości prawdziwej lub fałszywej (lub odwrotnie).
Jonathan Cast

1
@jcast Nie, włączenie bardziej wyraźne != NULL.
Miles Rout

1
Nie, bardziej jednoznacznie powiedzieć (x != NULL) != 0. Po tym wszystkim, to co C jest naprawdę sprawdzenie, prawda?
Jonathan Cast

@jcast Nie, nie jest. Sprawdzanie, czy coś jest nierównomierne do fałszu, nie polega na pisaniu warunkowej w jakimkolwiek języku.
Miles Rout

„Sprawdzanie, czy coś nie jest równe fałszowi, nie polega na tym, jak piszesz warunkowo w jakimkolwiek języku”. Dokładnie.
Jonathan Cast

2

W czasach K&R

  • „C” to przenośny kod zestawu
  • Był używany przez programistów myślących w kodzie asemblera
  • Kompilator nie dokonał dużej optymalizacji
  • Większość komputerów ma „złożone zestawy instrukcji”, na przykład while ((s[i++]=t[j++]) != '\0')odwzorowuje jedną instrukcję na większości procesorów (oczekuję Dec VAC)

Tam dni

  • Większość osób czytających kod C nie jest programistami kodu asemblera
  • Kompilatory C przeprowadzają wiele optymalizacji, dlatego łatwiejszy do odczytania kod prawdopodobnie zostanie przetłumaczony na ten sam kod maszynowy.

(Uwaga na temat zawsze używania nawiasów klamrowych - pierwszy zestaw kodu zajmuje więcej miejsca z powodu „niepotrzebnych” {}, z mojego doświadczenia często uniemożliwiają kod, który został źle scalony z kompilatora i pozwalają na błędy z nieprawidłowymi miejscami docelowymi „;” wykryte przez narzędzia.)

Jednak w dawnych czasach druga wersja kodu byłaby przeczytana. (Jeśli mam rację!)

concat(char* s, char *t){      
    while (*s++);
    --s;
    while (*s++=*t++);
}

2

Nawet bycie w stanie to zrobić, to bardzo zły pomysł. Nazywa się to potocznie „The Last Last Bug”:

if (alert = CODE_RED)
{
   launch_nukes();
}

Chociaż prawdopodobnie nie popełniasz tak poważnego błędu, bardzo łatwo jest przypadkowo zepsuć się i spowodować trudny do znalezienia błąd w bazie kodu. Większość nowoczesnych kompilatorów wstawi ostrzeżenie o zadaniach wewnątrz warunku. Są tam z jakiegoś powodu, a ty dobrze byś je posłuchał i po prostu uniknął tego konstruktu.


Przed tymi ostrzeżeniami napisalibyśmy, CODE_RED = alertaby dać błąd kompilatora.
Ian

4
@ Ian Yoda Uwarunkowane, które się nazywa. Są trudne do odczytania. Niefortunna jest dla nich konieczność.
Mason Wheeler

Po bardzo krótkim wprowadzającym okresie „przyzwyczajania się” warunki Yody nie są trudniejsze do odczytania niż normalne. Czasami są bardziej czytelne . Na przykład, jeśli masz sekwencję ifs / elseifs, posiadanie warunku testowanego na lewym pod kątem większego nacisku jest niewielką poprawą IMO.
user949300

2
@ user949300 Dwa słowa: syndrom sztokholmski: P
Mason Wheeler

2

Oba style są dobrze uformowane, poprawne i odpowiednie. To, który jest bardziej odpowiedni, zależy w dużej mierze od wytycznych stylu Twojej firmy. Nowoczesne środowiska IDE ułatwią korzystanie z obu stylów poprzez użycie podszewek składni na żywo, które wyraźnie podkreślają obszary, które mogłyby stać się źródłem pomieszania.

Na przykład Netbeans wyróżnia następujące wyrażenie :

if($a = someFunction())

na podstawie „przypadkowego przeniesienia”.

wprowadź opis zdjęcia tutaj

Aby wyraźnie powiedzieć Netbeansowi, że „tak, naprawdę chciałem to zrobić ...”, wyrażenie można zawrzeć w nawiasie.

if(($a = someFunction()))

wprowadź opis zdjęcia tutaj

Ostatecznie wszystko sprowadza się do wytycznych dotyczących stylu firmy i dostępności nowoczesnych narzędzi ułatwiających proces rozwoju.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.