Dlaczego warto używać końcowych znaków nowej linii zamiast przewijania za pomocą printf?

79

Słyszałem, że powinieneś unikać prowadzenia nowych linii podczas korzystania printf. Więc zamiast tego printf("\nHello World!")powinieneś użyćprintf("Hello World!\n")

W tym konkretnym przykładzie powyżej nie ma to sensu, ponieważ wynik byłby inny, ale rozważ to:

printf("Initializing");
init();
printf("\nProcessing");
process_data();
printf("\nExiting");

w porównaniu do:

printf("Initializing\n");
init();
printf("Processing\n");
process_data();
printf("Exiting");

Nie widzę żadnych korzyści z końcowymi znakami nowej linii, poza tym, że wygląda lepiej. Czy jest jakiś inny powód?

EDYTOWAĆ:

Zajmę się głosowaniem w tej sprawie tutaj i teraz. Nie sądzę, że należy to do przepełnienia stosu, ponieważ to pytanie dotyczy głównie projektowania. Powiedziałbym również, że chociaż mogą to być opinie w tej sprawie, odpowiedź Kiliana Fotha i odpowiedź cmastera dowodzą, że rzeczywiście jedno podejście przynosi bardzo obiektywne korzyści.

klutt
źródło
5
To pytanie jest na granicy między „problemami z kodem” (który jest nie na temat) a „koncepcyjnym projektowaniem oprogramowania” (który jest na temat). Może się zamknąć, ale nie bierz tego zbyt mocno. Myślę jednak, że dodanie konkretnych przykładów kodu było właściwym wyborem.
Kilian Foth,
46
Ostatni wiersz połączyłby się z wierszem poleceń w systemie Linux bez końca nowej linii.
GrandmasterB,
4
Jeśli „wygląda lepiej” i nie ma żadnych wad, to wystarczająco dobry powód, aby to zrobić, IMO. Pisanie dobrego kodu nie różni się niczym od napisania dobrej powieści lub dobrej pracy technicznej - diabeł zawsze tkwi w szczegółach.
alephzero,
5
Zrób init()i process_data()wydrukuj coś samodzielnie? Jak wyglądałby wynik, gdyby tak się stało?
Bergi,
9
\njest terminatorem linii , a nie separatorem linii . Dowodem na to jest fakt, że pliki tekstowe w systemie UNIX prawie zawsze kończą się na \n.
Jonathon Reinhart

Odpowiedzi:

222

Spora liczba terminali I / O jest buforowana w linii , więc kończąc komunikat \ n możesz mieć pewność, że zostanie on wyświetlony w odpowiednim czasie. W przypadku \ \ wiadomość może lub nie może być wyświetlana od razu. Często oznacza to, że każdy krok wyświetla komunikat postępu z poprzedniego kroku, co nie powoduje końca zamieszania i marnowania czasu, gdy próbujesz zrozumieć zachowanie programu.

Kilian Foth
źródło
20
Jest to szczególnie ważne, gdy używasz printf do debugowania programu, który ulega awarii. Umieszczenie nowej linii na końcu printf oznacza, że ​​standardowe wyjście do konsoli jest opróżniane przy każdym printf. (Zauważ, że gdy stdout jest przekierowywany do pliku, biblioteki std zwykle wykonują buforowanie bloków zamiast buforowania linii, dzięki czemu debugowanie printf jest dość trudne nawet przy nowej linii na końcu.)
Erik Eidt
25
@ErikEidt Zauważ, że powinieneś użyć fprintf(STDERR, …)zamiast tego, który zasadniczo nie jest buforowany dla danych diagnostycznych.
Deduplicator,
4
@Deduplicator Zapisywanie komunikatów diagnostycznych w strumieniu błędów ma również swoje wady - wiele skryptów zakłada, że ​​program się nie powiedzie, jeśli coś zostanie zapisane w strumieniu błędów.
Voo,
54
@Voo: Argumentowałbym, że każdy program, który zakłada zapis do stderr, wskazuje, że błąd sam w sobie jest niepoprawny. Kod zakończenia procesu wskazuje, czy nie powiódł się. Jeśli to była awaria, to wyjście stderr wyjaśni, dlaczego . Jeśli proces zakończy się pomyślnie (kod wyjścia zero), wyjście stderr powinno być uważane za wyjście informacyjne dla ludzkiego użytkownika, bez określonego semantycznego analizowalnego przez maszynę (może na przykład zawierać ostrzeżenia czytelne dla człowieka), podczas gdy stdout jest rzeczywistym wyjściem program, prawdopodobnie odpowiedni do dalszego przetwarzania.
Daniel Pryden,
23
@Voo: Jakie programy opisujesz? Nie znam żadnego powszechnie używanego pakietu oprogramowania, który zachowuje się tak, jak opisujesz. Wiem, że istnieją programy, które działają, ale to nie tak, że właśnie wymyśliłem konwencję, którą opisałem powyżej: tak działa ogromna większość programów w środowisku Unix lub podobnym do Unixa i, według mojej wiedzy, sposób, w jaki zdecydowana większość programów zawsze ma. Z pewnością nie zalecałbym, aby jakikolwiek program unikał pisania do stderr tylko dlatego, że niektóre skrypty nie radzą sobie z tym dobrze.
Daniel Pryden
73

W systemach POSIX (w zasadzie każdy Linux, BSD, dowolny system oparty na otwartym kodzie źródłowym), linia jest zdefiniowana jako ciąg znaków zakończony znakiem nowej linii \n. Jest to podstawowe założenie wszystkich standardowych narzędzi wiersza poleceń opierać, w tym (ale nie tylko) wc, grep, sed, awk, i vim. Jest to również powód, dla którego niektóre edytory (jak vim) zawsze dodają znak \nna końcu pliku i dlaczego wcześniejsze standardy C wymagały, aby nagłówki kończyły się \nznakiem.

Btw: Posiadanie \nzakończonych linii znacznie ułatwia przetwarzanie tekstu: Wiesz, że masz kompletną linię, kiedy masz ten terminator. I wiesz na pewno, że musisz spojrzeć na więcej postaci, jeśli jeszcze nie spotkałeś tego terminatora.

Oczywiście dzieje się tak po stronie wejściowej programów, ale dane wyjściowe programu są bardzo często używane jako dane wejściowe programu. Twoje dane wyjściowe powinny być więc zgodne z konwencją, aby umożliwić płynne wprowadzanie danych do innych programów.

cmaster
źródło
25
Jest to jedna z najstarszych debat w inżynierii oprogramowania: czy lepiej jest używać nowych linii (lub, w języku programowania, innego znacznika „końca instrukcji”, takiego jak średnik), jako terminatorów linii lub separatorów linii ? Oba podejścia mają swoje zalety i wady. Świat Windows przeważnie opierał się na pomyśle, że sekwencja nowej linii (zwykle tam CR LF) jest separatorem linii , a zatem ostatnia linia w pliku nie musi się z nią kończyć. Jednak w świecie uniksowym sekwencja nowej linii (LF) jest zakończeniem linii i wiele programów jest zbudowanych wokół tego założenia.
Daniel Pryden
33
POSIX definiuje nawet linię jako „Sekwencję zero lub więcej znaków nielinearnych plus znak kończący newline ”.
rura
6
Biorąc pod uwagę, że jak mówi @pipe, jest w specyfikacji POSIX, prawdopodobnie możemy to nazwać de jure, a nie de facto, jak sugeruje odpowiedź?
Baldrickk,
4
@Baldrickk Right. Zaktualizowałem teraz moją odpowiedź, aby była bardziej twierdząca.
cmaster
C czyni również tę konwencję dla plików źródłowych: niepusty plik źródłowy, który nie kończy się na nowej linii, powoduje niezdefiniowane zachowanie.
R ..
31

Oprócz tego, co inni wspominali, wydaje mi się, że istnieje znacznie prostszy powód: to standard. Ilekroć cokolwiek drukuje do STDOUT, prawie zawsze zakłada, że ​​jest już w nowej linii, a zatem nie musi zaczynać nowej. Zakłada również, że następny wiersz, który ma zostać napisany, będzie działał w ten sam sposób, więc pomocnie kończy się, rozpoczynając nowy wiersz.

Jeśli wypiszesz linie wiodące nowego wiersza przeplecione ze standardowymi liniami końcowymi nowej linii, ”będzie to wyglądać następująco:

Trailing-newline-line
Trailing-newline-line

Leading-newline-line
Leading-newline-line
Leading-newline-lineTrailing-newline-line
Trailing-newline-line

Leading-newline-lineTrailing-newline-line
Trailing-newline-line
Trailing-newline-line

... który prawdopodobnie nie jest tym, czego chcesz.

Jeśli użyjesz tylko wiodących znaków nowej linii w swoim kodzie i uruchomisz go tylko w środowisku IDE, może się okazać, że jest w porządku. Jak tylko uruchomisz go w terminalu lub wprowadzisz kod innej osoby, który napisze STDOUT wraz z twoim kodem, zobaczysz niepożądane wyjście jak wyżej.

Facet z kapeluszem
źródło
2
Coś podobnego dzieje się z programami, które są przerywane w interaktywnej powłoce - jeśli drukowana jest linia częściowa (brakuje jej nowej linii), wówczas powłoka nie jest pewna, w której kolumnie znajduje się kursor, co utrudnia edycję następnego wiersza poleceń. Chyba że dodasz do swojej linii wiodącą nową linię $PS1, która będzie drażniąca po konwencjonalnych programach.
Toby Speight
17

Ponieważ wysoko głosowane odpowiedzi podały już doskonałe techniczne powody, dla których preferowane powinny być końcowe linie, podejdę do tego z innej perspektywy.

Moim zdaniem poniższe uwagi sprawiają, że program jest bardziej czytelny:

  1. wysoki stosunek sygnału do szumu (inaczej prosty, ale nie prostszy)
  2. ważne pomysły są najważniejsze

Z powyższych punktów możemy argumentować, że końcowe znaki nowej linii są lepsze. Newlines formatują „szum” w porównaniu do wiadomości, wiadomość powinna się wyróżniać, a zatem powinna być na pierwszym miejscu (może również pomóc wyróżnianie składni).

Alex Vong
źródło
19
Tak, "ok\n"jest znacznie lepszy niż "\nok"...
cmaster
@cmaster: Przypomina mi o przeczytaniu o MacOS przy użyciu interfejsów API Pascal-string w C, co wymagało poprzedzenia wszystkich literałów ciągiem magicznym kodem ucieczki "\pFoobar".
grawity
16

Korzystanie ze znaku nowej linii upraszcza późniejsze modyfikacje.

Jako (bardzo trywialny) przykład oparty na kodzie OP, załóżmy, że musisz wygenerować jakieś dane wyjściowe przed komunikatem „Inicjalizacja”, a dane wyjściowe pochodzą z innej logicznej części kodu, w innym pliku źródłowym.

Po uruchomieniu pierwszego testu i stwierdzeniu, że „Inicjalizacja” jest teraz dołączana na końcu wiersza innego wyjścia, należy przeszukać kod, aby znaleźć miejsce, w którym został wydrukowany, a następnie mieć nadzieję, że zmieni się „Inicjalizacja” na „\ nInicjalizacja „nie psuje formatu czegoś innego, w różnych okolicznościach.

Teraz zastanów się, jak poradzić sobie z faktem, że twoje nowe wyjście jest w rzeczywistości opcjonalne, więc zmiana na „\ nInicjowanie” czasami powoduje niechcianą pustą linię na początku wyjścia ...

Czy ustawiasz flagę globalną ( wstrząs? wcześniejsze dane wyjściowe i pozostawiają przyszłe czytniki kodów zastanawiające się, dlaczego ta „inicjalizacja” nie ma wiodącego „\ n”, tak jak wszystkie inne komunikaty wyjściowe?

Jeśli konsekwentnie wypisujesz końcowe znaki nowej linii, w miejscu, w którym wiesz, że dotarłeś do końca linii, która musi zostać zakończona, omijasz wszystkie te problemy. Zauważ, że może to wymagać osobnej instrukcji put ("\ n") na końcu jakiejś logiki, która generuje wiersz po kawałku, ale chodzi o to, że wypisujesz nowy wiersz w najwcześniejszym miejscu w kodzie, gdzie wiesz, że musisz rób to, nie gdzie indziej.

alephzero
źródło
1
Jeśli każdy niezależny element wyjściowy ma pojawiać się w osobnej linii, końcowe nowe linie mogą działać poprawnie. Jeśli jednak praktyczne jest konsolidowanie wielu pozycji, sprawy stają się bardziej skomplikowane. Jeśli praktyczne jest wprowadzenie wszystkich danych wyjściowych za pomocą wspólnej procedury, operacja polegająca na wstawieniu czystego do końca wiersza, jeśli ostatnim znakiem był CR, nic, jeśli ostatni znak był nową linią, a nową linią, jeśli ostatnim znakiem był czymkolwiek innym, może być pomocny, jeśli programy muszą zrobić coś innego niż wypisać ciąg niezależnych linii.
supercat,
7

Dlaczego warto używać końcowych znaków nowej linii zamiast przewijania za pomocą printf?

Dokładnie dopasuj specyfikację C

Biblioteka C definiuje linię jako kończącą się znakiem nowej linii '\n' .

Strumień tekstowy to uporządkowana sekwencja znaków składająca się z linii , przy czym każda linia zawiera zero lub więcej znaków plus znak kończący nowy wiersz. To, czy ostatni wiersz wymaga zakończenia znaku nowej linii, jest zdefiniowane w implementacji. C11 §7.21.2 2

Kod, który zapisuje dane jako linie, będzie wówczas pasował do tej koncepcji biblioteki.

printf("Initializing"); // Write part of a line
printf("\nProcessing"); // Finish prior line & write part of a line
printf("\nExiting");    // Finish prior line & write an implementation-defined last line

printf("Initializing\n");//Write a line 
printf("Processing\n");  //Write a line
printf("Exiting");       //Write an implementation-defined last line

Re: ostatni wiersz wymaga zakończenia znaku nowej linii . Polecam zawsze pisać końcowy '\n'wynik i tolerować jego brak na wejściu.


Sprawdzanie pisowni

Mój moduł sprawdzania pisowni narzeka. Być może twoje też.

  v---------v Not a valid word
"\nProcessing"

 v--------v OK
"Processing\n");
chux
źródło
Kiedyś poprawiłem się, ispell.elaby lepiej sobie z tym poradzić. Przyznaję, że częściej \tbył to problem i można było tego uniknąć, dzieląc ciąg na wiele tokenów, ale był to tylko efekt uboczny bardziej ogólnej pracy polegającej na „ignorowaniu”, polegającej na selektywnym pomijaniu nietekstowych części HTML lub ciała wieloczęściowe MIME i części kodu bez komentarzy. Zawsze chciałem rozszerzyć go na przełączanie języków, w których są odpowiednie metadane (np. <p lang="de_AT">Lub Content-Language: gd), ale nigdy nie otrzymałem Round Tuit. A opiekun całkowicie odrzucił moją łatkę. :-(
Toby Speight
@TobySpeight Oto okrągły toit . Czekamy na wypróbowanie Twojego ulepszonego ispell.el.
chux
4

Wiodące znaki nowej linii często ułatwiają pisanie kodu, gdy występują warunki warunkowe, na przykład

printf("Initializing");
if (jobName != null)
    printf(": %s", jobName);
init();
printf("\nProcessing");

(Ale jak zauważono w innym miejscu, może być konieczne opróżnienie bufora wyjściowego przed wykonaniem jakichkolwiek czynności, które zajmują dużo czasu procesora).

W związku z tym można przedstawić dobry przypadek dla obu sposobów zrobienia tego, jednak osobiście nie lubię printf () i użyłbym niestandardowej klasy do zbudowania wyniku.

Ian
źródło
1
Czy możesz wyjaśnić, dlaczego ta wersja jest łatwiejsza do napisania niż ta z końcowymi znakami nowej linii? W tym przykładzie nie jest to dla mnie oczywiste. Zamiast tego mogłem zobaczyć problemy wynikające z dodania następnego wyjścia do tego samego wiersza co "\nProcessing".
Raimund Krämer
Podobnie jak Raimund, widzę również problemy wynikające z takiej pracy. Podczas rozmowy należy wziąć pod uwagę otaczające wydruki printf. Co jeśli chcesz warunkować całą linię „Inicjalizacja”? Musisz podać w tym stanie wiersz „Przetwarzanie”, aby wiedzieć, czy powinieneś przedrostkiem nowego wiersza, czy nie. Jeśli przed tobą jest inny wydruk i musisz uwarunkować wiersz „Przetwarzanie”, musisz również dołączyć następny wydruk w tym stanie, aby wiedzieć, czy powinieneś przedrostek z nowym znakiem nowej linii i tak dalej dla każdego wydruku.
JoL
2
Zgadzam się z zasadą, ale przykład nie jest dobry. Bardziej odpowiednim przykładem może być kod, który ma wyprowadzać pewną liczbę elementów w wierszu. Jeśli wyjście ma zaczynać się od nagłówka, który kończy się nową linią, a każda linia ma zaczynać się od nagłówka, łatwiej jest powiedzieć, np. if ((addr & 0x0F)==0) printf("\n%08X:", addr);I bezwarunkowo dodać nowy wiersz na końcu, niż użyć oddzielny kod dla nagłówka każdego wiersza i końcowego nowego wiersza.
supercat
1

Wiodące znaki nowej linii nie działają dobrze z innych funkcji bibliotecznych, w szczególności puts()i perrorw Bibliotece standardzie, ale także każdej innej biblioteki najprawdopodobniej w użyciu.

Jeśli chcesz wydrukować wcześniej napisaną linię (stałą lub już sformatowaną - np. Za pomocą sprintf()), to puts()jest naturalny (i wydajny) wybór. Jednak nie ma sposobu, puts()aby zakończyć poprzednią linię i napisać linię niezakończoną - zawsze zapisuje terminator linii.

Toby Speight
źródło