Czy zmienne globalne są złe w Arduino?

24

Jestem stosunkowo nowy w programowaniu i wiele dobrych praktyk kodowania, które skutecznie czytam, stwierdza, że ​​istnieje bardzo niewiele dobrych powodów, aby używać zmiennej globalnej (lub że najlepszy kod w ogóle nie ma globalizacji).

Starałem się o tym pamiętać, pisząc oprogramowanie do tworzenia interfejsu Arduino z kartą SD, mówienia do komputera i uruchamiania sterownika silnika.

Obecnie mam 46 globali na około 1100 linii kodu „poziomu początkującego” (żadna linia nie ma więcej niż jednej akcji). Czy to dobry stosunek, czy powinienem raczej bardziej go zmniejszyć? Jakie praktyki mogę zastosować, aby jeszcze bardziej zmniejszyć liczbę globali?

Pytam o to tutaj, ponieważ szczególnie interesują mnie najlepsze praktyki kodowania produktów Arduino, a nie ogólnie programowanie komputerowe.

ATE-ENGE
źródło
2
W Arduino nie można uniknąć zmiennych globalnych. Każda deklaracja zmiennej poza zakresem funkcji / metody ma charakter globalny. Jeśli więc chcesz dzielić wartości między funkcje, muszą one być globalne, chyba że chcesz przekazać każdą wartość jako argument.
16
@LookAlterno Err, nie możesz pisać klas w Ardunio, ponieważ jest to po prostu C ++ z dziwnymi makrami i bibliotekami? Jeśli tak, to nie każda zmienna ma charakter globalny. I nawet w C zwykle uważa się za najlepszą praktykę preferowanie przekazywania zmiennych (być może wewnątrz struktur) do funkcji zamiast posiadania zmiennych globalnych. Może to być mniej wygodne dla małego programu, ale zwykle się opłaca, gdy program staje się większy i bardziej złożony.
Muzer
11
@LookAlterno: Unikam i „Nie możesz” to bardzo różne rzeczy.
Lekkość ściga się z Moniką
2
W rzeczywistości niektórzy programiści osadzeni blokują zmienne lokalne i zamiast tego wymagają zmiennych globalnych (lub zmiennych statycznych o zasięgu funkcji). Gdy programy są małe, może być łatwiej analizowane jako prosta maszyna stanu; ponieważ zmienne nigdy się nie nadpisują (tzn. gdy stosują zmienne przydzielane do stosu).
Rob
1
Zmienne statyczne i globalne mają tę zaletę, że znają zużycie pamięci w czasie kompilacji. Przy arduino o bardzo ograniczonej dostępnej pamięci może to być zaletą. Nowicjuszowi dość łatwo jest wyczerpać dostępną pamięć i doświadczyć niemożliwych do wykrycia awarii.
antipattern

Odpowiedzi:

33

Nie są one same w sobie złe , ale mają tendencję do nadmiernego wykorzystywania tam, gdzie nie ma dobrego powodu, aby z nich korzystać.

Wiele razy zmienne globalne są korzystne. Zwłaszcza, że ​​programowanie Arduino różni się znacznie od programowania komputera.

Największą korzyścią dla zmiennych globalnych jest alokacja statyczna. Zwłaszcza w przypadku dużych i złożonych zmiennych, takich jak instancje klas. Dynamiczna alokacja (użycie newitp.) Jest niezadowolona z powodu braku zasobów.

Ponadto nie otrzymujesz ani jednego drzewa wywołań, jak w normalnym programie C (pojedyncza main()funkcja wywołująca inne funkcje) - zamiast tego skutecznie uzyskujesz dwa oddzielne drzewa ( setup()wywoływanie funkcji, a następnie loop()wywoływanie funkcji), co oznacza, że ​​czasami zmiennymi globalnymi są jedyny sposób na osiągnięcie celu (tj. jeśli chcesz go używać zarówno w, jak setup()i loop()).

Więc nie, nie są źli, a na Arduino mają więcej i lepsze zastosowania niż na PC.

Majenko
źródło
Ok, co jeśli jest to coś, czego używam tylko w loop()(lub w wielu wywoływanych funkcjach loop())? czy lepiej byłoby je skonfigurować inaczej niż zdefiniować na początku?
ATE-ENGE
1
W takim przypadku prawdopodobnie zdefiniowałbym je w loop () (być może tak static, jakbym potrzebował, aby zachować swoją wartość w iteracjach) i przekazałbym je przez parametry funkcji w dół łańcucha wywołań.
Majenko
2
To jest jeden sposób, lub void foo(int &var) { var = 4; }foo(n);n
podaj
1
Klasy można przypisywać do stosu. Wprawdzie umieszczanie dużych instancji na stosie nie jest dobre, ale nadal.
JAB
1
@JAB Wszystko można przypisać do stosu.
Majenko
18

Bardzo trudno jest udzielić ostatecznej odpowiedzi, nie widząc rzeczywistego kodu.

Zmienne globalne nie są złe i często mają sens w środowisku osadzonym, w którym zazwyczaj uzyskuje się duży dostęp do sprzętu. Masz tylko cztery UARTS, tylko jeden port I2C itp. Dlatego warto używać globałów dla zmiennych powiązanych z konkretnymi zasobami sprzętowymi. I rzeczywiście, biblioteki Arduino rdzeń robi, że: Serial, Serial1, itd. Są zmienne globalne. Ponadto zmienna reprezentująca globalny stan programu jest zazwyczaj globalna.

Obecnie mam 46 globali na około 1100 linii [kodu]. Czy to dobry stosunek [...]

Nie chodzi o liczby. Właściwe pytanie, które powinieneś sobie zadać, brzmi: dla każdego z tych globałów, czy sensowne jest mieć je w zasięgu globalnym.

Mimo to 46 globalnych wydaje mi się trochę za wysokie. Jeśli niektóre z nich przechowują stałe wartości, należy je zakwalifikować jako const: kompilator zazwyczaj zoptymalizuje ich pamięć. Jeśli którakolwiek z tych zmiennych jest używana tylko w obrębie jednej funkcji, ustaw ją lokalnie. Jeśli chcesz, aby jego wartość utrzymywała się między wywołaniami funkcji, zakwalifikuj ją jako static. Możesz także zmniejszyć liczbę „widocznych” globałów, grupując zmienne w obrębie klasy i mając jedną globalną instancję tej klasy. Ale rób to tylko wtedy, gdy ma sens poskładanie rzeczy. Stworzenie dużej GlobalStuffklasy ze względu na posiadanie tylko jednej zmiennej globalnej nie pomoże uczynić twojego kodu bardziej przejrzystym.

Edgar Bonet
źródło
1
Dobrze! Nie wiedziałem o tym, staticczy mam zmienną, która jest używana tylko w jednej funkcji, ale otrzymuję nową wartość za każdym razem, gdy funkcja jest wywoływana (jak var=millis()), czy powinienem to zrobić static?
ATE-ENGE
3
Nie. staticTylko wtedy, gdy musisz zachować wartość. Jeśli pierwszą rzeczą, którą robisz w funkcji, jest ustawienie wartości zmiennej na bieżący czas, nie ma potrzeby zachowywania starej wartości. Wszystko, co statyczne robi w tej sytuacji, to marnowanie pamięci.
Andrew
Jest to bardzo wszechstronna odpowiedź, wyrażająca oba punkty na korzyść globalizmu i sceptycyzm. +1
underscore_d
6

Głównym problemem związanym ze zmiennymi globalnymi jest utrzymanie kodu. Podczas odczytywania wiersza kodu łatwo jest znaleźć deklarację zmiennych przekazywanych jako parametr lub deklarowanych lokalnie. Nie jest tak łatwo znaleźć deklarację zmiennych globalnych (często wymaga i IDE).

Kiedy masz wiele zmiennych globalnych (40 to już dużo), trudno jest mieć wyraźną nazwę, która nie jest zbyt długa. Korzystanie z przestrzeni nazw jest sposobem na wyjaśnienie roli zmiennych globalnych.

Zły sposób naśladowania przestrzeni nazw w C to:

static struct {
    int motor1, motor2;
    bool sensor;
} arm;

W procesorze Intel lub uzbrojonym dostęp do zmiennych globalnych jest wolniejszy niż w przypadku innych zmiennych. Prawdopodobnie na Arduino jest odwrotnie.

BOC
źródło
2
W AVR globale znajdują się w pamięci RAM, podczas gdy większość niestatystycznych lokalizacji lokalnych jest przydzielana do rejestrów CPU, co przyspiesza ich dostęp.
Edgar Bonet
1
Problemem nie jest tak naprawdę znalezienie deklaracji; umiejętność szybkiego zrozumienia kodu jest ważna, ale ostatecznie drugorzędna w stosunku do tego, co robi - i tam prawdziwym problemem jest znalezienie, który warunek, w której nieznana część twojego kodu jest w stanie użyć globalnej deklaracji do robienia dziwnych rzeczy z pominiętym obiektem otwarte dla wszystkich, aby robić, co chcą.
underscore_d
5

Chociaż nie używałbym ich podczas programowania na PC, dla Arduino mają one pewne zalety. Większość, jeśli zostało już powiedziane:

  • Brak dynamicznego użycia pamięci (tworzenie luk w ograniczonej przestrzeni sterty Arduino)
  • Dostępne wszędzie, dlatego nie trzeba podawać ich jako argumentów (co kosztuje miejsce na stosie)

Ponadto w niektórych przypadkach, szczególnie pod względem wydajności, dobrze jest używać zmiennych globalnych zamiast tworzyć elementy w razie potrzeby:

  • Aby zmniejszyć luki w przestrzeni sterty
  • Dynamiczne przydzielanie i / lub zwalnianie pamięci może zająć dużo czasu
  • Zmienne mogą być „ponownie używane”, podobnie jak lista elementów globalnej listy, które są używane z wielu powodów, np. Raz jako bufor dla SD, a później jako tymczasowy bufor ciągów.
Michel Keijzers
źródło
5

Podobnie jak wszystko (oprócz goto, które są naprawdę złe), globały mają swoje miejsce.

np. jeśli masz debugowany port szeregowy lub plik dziennika, do którego musisz być w stanie pisać z dowolnego miejsca, często warto nadać mu charakter globalny. Podobnie, jeśli masz jakieś krytyczne informacje o stanie systemu, to uczynienie go globalnym jest często najłatwiejszym rozwiązaniem. Nie ma sensu mieć wartości, którą musisz przekazać każdej funkcji w programie.

Jak powiedzieli inni, 46 wydaje się dużo za niewiele ponad 1000 wierszy kodu, ale bez znajomości szczegółów tego, co robisz, trudno powiedzieć, czy już ich używasz, czy nie.

Jednak dla każdego globu zadaj sobie kilka ważnych pytań:

Czy nazwa jest jasna i konkretna, aby przypadkowo nie próbować użyć tej samej nazwy w innym miejscu? Jeśli nie, zmień nazwę.

Czy to musi się kiedykolwiek zmienić? Jeśli nie, zastanów się nad ustawieniem go jako const.

Czy musi to być widoczne wszędzie, czy tylko globalne, aby wartość była zachowywana między wywołaniami funkcji? Rozważ więc uczynienie go lokalnym dla funkcji i użycie słowa kluczowego static.

Czy coś naprawdę się popsuje, jeśli zmieni się to przez fragment kodu, gdy nie będę ostrożny? np. jeśli masz dwie powiązane zmienne, powiedz imię i numer identyfikacyjny, które są globalne (patrz poprzednia uwaga na temat korzystania z globalnej, gdy potrzebujesz informacji prawie wszędzie), to jeśli jedna z nich zostanie zmieniona bez innych nieprzyjemnych rzeczy, może się zdarzyć. Tak, możesz być ostrożny, ale czasem dobrze jest nieco egzekwować ostrożność. np. umieść je w innym pliku .c, a następnie zdefiniuj funkcje, które ustawiają oba z nich jednocześnie i pozwalają je odczytać. Następnie włączasz tylko funkcje do powiązanego pliku nagłówka, w ten sposób reszta twojego kodu może uzyskiwać dostęp do zmiennych tylko przez zdefiniowane funkcje, a więc nie może popsuć rzeczy.

- aktualizacja - Właśnie zdałem sobie sprawę, że zapytałeś o najlepsze praktyki specyficzne dla Arduino, a nie o ogólne kodowanie i jest to raczej ogólna odpowiedź na kodowanie. Ale szczerze mówiąc, nie ma dużej różnicy, dobra praktyka to dobra praktyka. startup()I loop()struktura środków Arduino, że masz do wykorzystania globalnych trochę więcej niż inne platformy, w niektórych sytuacjach, ale to naprawdę nie zmienia wiele, zawsze kończy się dążenie do najlepszych można zrobić w ramach ograniczeń platformy bez względu na to, co platforma jest.

Andrzej
źródło
Nic nie wiem o Arduinos, ale dużo zajmuję się tworzeniem komputerów i serwerów. Jest jedno dopuszczalne użycie (IMHO) dla gotos i to jest wyjście z zagnieżdżonych pętli, jest znacznie czystsze i łatwiejsze do zrozumienia niż alternatywy.
Trwałość
Jeśli uważasz, że gototo zło, sprawdź kod systemu Linux. Prawdopodobnie są tak samo złe jak try...catchbloki, gdy są używane jako takie.
Dmitrij Grigoryev
5

Czy są źli? Może. Problem z globalnymi polega na tym, że mogą być one dostępne i modyfikowane w dowolnym momencie przez dowolną wykonywaną funkcję lub fragment kodu, bez ograniczeń. Może to prowadzić do sytuacji, które, powiedzmy, są trudne do prześledzenia i wyjaśnienia. Pożądane jest zatem zminimalizowanie ilości globali, jeśli to możliwe, przywrócenie tej wartości do zera.

Czy można tego uniknąć? Prawie zawsze tak. Problem z Arduino polega na tym, że zmuszają cię do tego dwufunkcyjnego podejścia, w którym zakładają ciebie setup()i ciebie loop(). W tym konkretnym przypadku nie masz dostępu do zakresu funkcji wywołującej tych dwóch funkcji (prawdopodobnie main()). Gdybyś to zrobił, byłbyś w stanie pozbyć się wszystkich globali i zamiast tego użyć miejscowych.

Wyobraź sobie następujące:

int main() {
  setup();

  while (true) {
    loop();
  }
  return 0;
}

To prawdopodobnie mniej więcej tak wygląda główna funkcja programu Arduino. Zmienne, których potrzebujesz zarówno w funkcji, jak setup()i w loop()funkcji, byłyby wówczas najlepiej zadeklarowane w zakresie main()funkcji niż w zakresie globalnym. Można je następnie udostępnić dwóm pozostałym funkcjom, przekazując je jako argumenty (w razie potrzeby używając wskaźników).

Na przykład:

int main() {
  int myVariable = 0;
  setup(&myVariable);

  while (true) {
    loop(&myVariable);
  }
  return 0;
}

Pamiętaj, że w tym przypadku musisz również zmienić podpis obu funkcji.

Ponieważ może to nie być wykonalne ani pożądane, widzę naprawdę tylko jeden sposób na usunięcie większości globali z programu Arduino bez modyfikowania struktury programu wymuszonego.

Jeśli dobrze pamiętam, możesz doskonale używać C ++ podczas programowania dla Arduino, a nie C. Jeśli nie znasz (jeszcze) OOP (Object Oriented Programming) lub C ++, może to trochę przyzwyczaić się i trochę czytanie.

Moją propozycją byłoby utworzenie klasy Program i utworzenie pojedynczej globalnej instancji tej klasy. Klasę należy uznać za plan dla obiektów.

Rozważ następujący przykładowy program:

class Program {
public:      
  Program();

  void setup();
  void loop();

private:
  int myFirstSampleVariable;
  int mySecondSampleVariable;
};

Program::Program() :
  myFirstSampleVariable(0),
  mySecondSampleVariable(0)
{

}

void Program::setup() {
  // your setup code goes here
}

void Program::loop() {
  // your loop code goes here
}

Program program; // your single global

void setup() {
  program.setup();
}

void loop() {
  program.loop();
}

Voilà, pozbyliśmy się prawie wszystkich globali. Funkcje, w których chcesz zacząć dodawać logikę aplikacji, to funkcje Program::setup()i Program::loop(). Funkcje te mają dostęp do zmiennych składowych specyficznych dla instancji, myFirstSampleVariablea mySecondSampleVariabletradycyjne setup()i loop()funkcje nie mają dostępu, ponieważ zmienne te zostały oznaczone jako prywatne. Ta koncepcja nazywa się enkapsulacją lub ukrywaniem danych.

Nauczenie cię OOP i / lub C ++ jest trochę poza zakresem odpowiedzi na to pytanie, więc zatrzymam się tutaj.

Podsumowując: globałów należy unikać i prawie zawsze można drastycznie zmniejszyć ich liczbę. Również podczas programowania dla Arduino.

Co najważniejsze, mam nadzieję, że moja odpowiedź będzie dla ciebie przydatna :)

Arjen
źródło
Jeśli chcesz, możesz zdefiniować własne main () w szkicu. Tak wygląda ta fotografia: github.com/arduino/Arduino/blob/1.8.3/hardware/arduino/avr/…
per1234
Singleton jest w rzeczywistości globalny, po prostu przebrany w wyjątkowo mylący sposób. Ma te same wady.
patstew
@patstew Czy masz coś przeciwko wytłumaczeniu mi, jak według ciebie ma to te same wady? Moim zdaniem tak nie jest, ponieważ możesz wykorzystać enkapsulację danych na swoją korzyść.
Arjen
@ per1234 Dzięki! Zdecydowanie nie jestem ekspertem od Arduino, ale przypuszczam, że moja pierwsza sugestia mogłaby również wtedy zadziałać.
Arjen
2
Cóż, nadal jest to stan globalny, do którego można uzyskać dostęp w dowolnym miejscu programu, wystarczy uzyskać do niego dostęp Program::instance().setup()zamiast globalProgram.setup(). Umieszczenie pokrewnych zmiennych globalnych w jednej klasie / strukturze / przestrzeni nazw może być korzystne, szczególnie jeśli są potrzebne tylko przez kilka powiązanych funkcji, ale wzorzec singletonu niczego nie dodaje. Innymi słowy, static Program p;ma globalną pamięć i static Program& instance()globalny dostęp, co równa się po prostu Program globalProgram;.
patstew
4

Zmienne globalne nigdy nie są złe . Ogólna zasada przeciwko nim jest tylko kulą, która pozwala przetrwać wystarczająco długo, aby zdobyć doświadczenie i podejmować lepsze decyzje.

Czym jest zmienna globalna, jest nieodłącznym założeniem, że istnieje tylko jedna rzecz (nie ma znaczenia, czy mówimy o globalnej tablicy lub mapie, która może zawierać wiele rzeczy, która nadal zawiera założenie, że istnieje tylko jedna taka lista lub mapowanie, a nie wiele niezależnych).

Zanim więc skorzystasz z globalnego, musisz zadać sobie pytanie: czy możliwe jest, że kiedykolwiek będę chciał użyć więcej niż jednej z tych rzeczy? Jeśli okaże się to prawdą, będziesz musiał zmodyfikować kod, aby cofnąć globalizację, i prawdopodobnie przekonasz się, że inne części twojego kodu zależą od tego założenia wyjątkowości, więc Będę musiał je również naprawić, a proces stanie się nudny i podatny na błędy. Naucza się „Nie używaj globałów”, ponieważ zazwyczaj jest to dość niewielki koszt unikania globałów od samego początku i pozwala uniknąć późniejszej konieczności poniesienia dużych kosztów.

Ale upraszczające założenia, na które pozwalają globale, również zmniejszają, przyspieszają i zużywają mniej pamięci, ponieważ nie musi ona omijać pojęcia, z czego korzysta, nie musi robić pośrednictwa, nie musi weź pod uwagę możliwość, że pożądana rzecz może nie istnieć itp. We wbudowanym oprogramowaniu istnieje większe prawdopodobieństwo ograniczenia rozmiaru kodu i / lub czasu procesora i / lub pamięci niż na komputerze, więc te oszczędności mogą mieć znaczenie. Wiele wbudowanych aplikacji ma również większą sztywność w zakresie wymagań - wiesz, że twój układ ma tylko jedno urządzenie peryferyjne, użytkownik nie może po prostu podłączyć innego do portu USB lub czegoś takiego.

Innym częstym powodem, dla którego chce się czegoś więcej niż jednego, co wydaje się wyjątkowe, jest testowanie - testowanie interakcji między dwoma komponentami jest łatwiejsze, gdy można po prostu przekazać instancję testową jakiegoś komponentu do funkcji, podczas gdy próba modyfikacji zachowania komponentu globalnego jest trudniejsza propozycja. Ale testowanie w świecie osadzonym zwykle bardzo różni się od innych, więc może to nie dotyczyć ciebie. O ile mi wiadomo, Arduino nie ma żadnej kultury testowej.

Więc śmiało i używaj globałów, gdy wydają się one opłacalne. Policja kodowa nie przyjdzie i cię nie dopadnie. Po prostu wiedz, że zły wybór może spowodować dla ciebie o wiele więcej pracy, więc jeśli nie jesteś pewien ...

Hobbs
źródło
0

Czy zmienne globalne są złe w Arduino?

nic nie jest z natury złe, w tym zmienne globalne. Scharakteryzowałbym to jako „zło konieczne” - może znacznie ułatwić ci życie, ale należy do niego podchodzić ostrożnie.

Jakie praktyki mogę zastosować, aby jeszcze bardziej zmniejszyć liczbę globali?

użyj funkcji otoki, aby uzyskać dostęp do zmiennych globalnych. więc przynajmniej zarządzasz nim z perspektywy zakresu.

dannyf
źródło
3
Jeśli używasz funkcji otoki, aby uzyskać dostęp do zmiennych globalnych, równie dobrze możesz umieścić swoje zmienne w tych funkcjach.
Dmitrij Grigoryev,