Jak mogę się upewnić, że fragment kodu działa tylko raz?


18

Mam kod, który chcę uruchomić tylko raz, nawet jeśli okoliczności, które go uruchomiły, mogą się zdarzyć wiele razy.

Na przykład, gdy użytkownik kliknie myszą, chcę kliknąć rzecz:

void Update() {
    if(mousebuttonpressed) {
        ClickTheThing(); // I only want this to happen on the first click,
                         // then never again.
    }
}

Jednak z tym kodem za każdym razem, gdy klikam myszą, rzecz zostaje kliknięta. Jak mogę to zrobić tylko raz?


7
Właściwie uważam to za zabawne, ponieważ nie sądzę, aby mieściło się w „specyficznych dla gry problemach programistycznych”. Ale tak naprawdę nigdy nie podobała mi się ta zasada.
ClassicThunder

3
@ClassicThunder Tak, z pewnością jest to bardziej ogólna strona programowania. Głosuj na zamknięcie, jeśli chcesz, zamieściłem to pytanie, abyśmy mieli coś do wskazania ludziom, gdy zadadzą podobne pytania. W tym celu może być otwarty lub zamknięty.
MichaelHouse

Odpowiedzi:


41

Użyj flagi logicznej.

W pokazanym przykładzie zmodyfikujesz kod tak, aby wyglądał następująco:

//a boolean flag that lets us "remember" if this thing has happened already
bool thatThingHappened = false;

void Update() {
    if(!thatThingHappened && mousebuttonpressed) {
        //if that thing hasn't happened yet and the mouse is pressed
        thatThingHappened = true;
        ClickTheThing();
    }
}

Ponadto, jeśli chcesz móc powtarzać akcję, ale ogranicz częstotliwość akcji (tj. Minimalny czas między kolejnymi akcjami). Zastosowałbyś podobne podejście, ale zresetowałeś flagę po pewnym czasie. Zobacz moją odpowiedź tutaj, aby uzyskać więcej pomysłów na ten temat.


1
Oczywista odpowiedź na twoje pytanie :-) Byłbym zainteresowany, aby zobaczyć alternatywne podejścia, jeśli ty lub ktokolwiek inny o nich wie. To użycie globalnego logika do tego zawsze śmierdziało dla mnie.
Evorlor,

1
@Evorlor Nie dla wszystkich oczywiste :). Interesują mnie również alternatywne rozwiązania, może nauczymy się czegoś nie tak oczywistego!
MichaelHouse

2
@Evorlor jako alternatywa, możesz użyć delegatów (wskaźników funkcji) i zmienić podejmowane działanie. np onStart() { delegate = doSomething; } ... if(mousebuttonpressed) { delegate.Execute(); }i void doSomething() { doStuff(); delegate = funcDoNothing; }, ale w końcu wszystkie opcje w końcu o flagę niektórych rodzajów masz włączony / wyłączony ... i delegata w tym przypadku to nic innego (z wyjątkiem, jeśli masz więcej niż dwie opcje co się dzieje być może?).
wondra,

2
@wondra Tak, to jest sposób. Dodaj to. Równie dobrze moglibyśmy sporządzić listę możliwych rozwiązań tego pytania.
MichaelHouse

1
@JamesSnell Bardziej prawdopodobne, jeśli był wielowątkowy. Ale wciąż dobra praktyka.
MichaelHouse

21

Jeśli flaga bool nie wystarczy lub chcesz poprawić czytelność * kodu w void Update()metodzie, możesz rozważyć użycie delegatów (wskaźników funkcji):

public class InputController 
{
  //declare delegate type:
  //<accessbility> delegate <return type> <name> ( <parameter1>, <paramteter2>, ...)
  public delegate void ClickAction();

  //declare a variable of that type
  public ClickAction ClickTheThing { get; set; }

  void onStart()
  {
    //assign it the method you wish to execute on first click
    ClickTheThing = doStuff;
  }

  void Update() {
    if(mousebuttonpressed) {
        //you simply call the delegate here, the currently assigned function will be executed
        ClickTheThing();
     }
  }

  private void doStuff()
  {
    //some logic here
    ClickTheThing = doNothing; //and set the delegate to other(empty) funtion
  }

  //or do something else
  private void doNothing(){}
}

W przypadku zwykłego polecenia „wykonaj raz” delegaci są przesadzeni, dlatego sugerowałbym użycie flagi bool.
Jeśli jednak potrzebujesz bardziej skomplikowanej funkcjonalności, delegaci są prawdopodobnie lepszym wyborem. Na przykład, jeśli chcesz wykonać więcej różnych działań: jedno przy pierwszym kliknięciu, drugie na drugim i jeszcze jedno na trzecim, możesz po prostu zrobić:

func1() 
{
  //do logic 1 here
  someDelegate = func2;
}

func2() 
{
  //do logic 2 here
  someDelegate = func3;
}
//etc...

zamiast dręczyć swój kod dziesiątkami różnych flag.
* kosztem niższej możliwości konserwacji pozostałej części kodu


Zrobiłem trochę profilowania z wynikami, tak jak się spodziewałem:

----------------------------------------
|     Method     |  Unity   |    C++   |
| -------------------------------------|
| positive flag  |  21 ms   |   6 ms   |
| negative flag  |  5 ms    |   7 ms   |
| delegate       |  25 ms   |   14 ms  |
----------------------------------------

Pierwszy test został przeprowadzony na Unity 5.1.2, mierzonym System.Diagnostics.Stopwatchw 32-bitowym projekcie (nie w projektancie!). Drugi na Visual Studio 2015 (v140) skompilowany w 32-bitowym trybie wydania z flagą / Ox. Oba testy przeprowadzono na procesorze Intel i5-4670K przy częstotliwości 3,4 GHz, z 10 000 000 iteracji dla każdej implementacji. kod:

//positive flag
if(flag){
    doClick();
    flag = false;
}
//negative flag
if(!flag){ }
else {
    doClick();
    flag = false;
}
//delegate
action();

wniosek: Podczas gdy kompilator Unity wykonuje dobrą robotę, optymalizując wywołania funkcji, dając mniej więcej taki sam wynik zarówno dla flagi dodatniej, jak i delegatów (odpowiednio 21 i 25 ms), niepoprawne przewidywanie gałęzi lub wywołanie funkcji jest wciąż dość drogie (uwaga: należy przyjąć, że pamięć podręczna jest delegowana w tym teście).
Co ciekawe, kompilator Unity nie jest wystarczająco inteligentny, aby zoptymalizować gałąź, gdy pojawi się 99 milionów nieprzewidzianych błędów, więc ręczne negowanie testu daje pewien wzrost wydajności, dając najlepszy wynik 5 ms. Wersja C ++ nie wykazuje żadnego wzrostu wydajności w celu zanegowania warunku, jednak ogólny koszt wywołania funkcji jest znacznie niższy.
co najważniejsze: różnica jest praktycznie nieistotna dla każdego scenariusza w świecie rzeczywistym


4
AFAIK jest to również lepsze z punktu widzenia wydajności. Wywołanie funkcji no-op, zakładając, że funkcja znajduje się już w pamięci podręcznej instrukcji, jest znacznie szybsze niż sprawdzanie flag, szczególnie gdy sprawdzanie jest zagnieżdżone w innych warunkach warunkowych.
Inżynier

2
Myślę, że Navin ma rację, prawdopodobnie będę mieć gorszą wydajność niż sprawdzanie flagi logicznej - chyba że twój JIT jest naprawdę inteligentny i zastąpi całą funkcję dosłownie niczym. Ale jeśli nie profilujemy wyników, nie możemy być tego pewni.
wondra

2
Hmm, ta odpowiedź przypomina mi ewolucję inżyniera SW . Żartujmy na bok, jeśli absolutnie potrzebujesz (i jesteś pewien) tego zwiększenia wydajności, idź na całość. Jeśli nie, proponuję zachować KISS i użyć flagi logicznej, jak zaproponowano w innej odpowiedzi . Jednak +1 za reprezentowaną tutaj alternatywę!
mucaho

1
Jeśli to możliwe, unikaj warunkowych. Mówię o tym, co dzieje się na poziomie komputera, niezależnie od tego, czy jest to twój własny kod macierzysty, kompilator JIT, czy interpreter. Trafienie pamięci podręcznej L1 jest mniej więcej takie samo jak trafienie do rejestru, być może nieco wolniejsze, tj. 1 lub 2 cykle, podczas gdy nieprzewidywalność gałęzi, która zdarza się rzadziej, ale wciąż średnio za dużo, kosztuje 10-20 cykli. Zobacz odpowiedź Jalfa: stackoverflow.com/questions/289405/… A to: stackoverflow.com/questions/11227809/…
Inżynier

1
Pamiętaj ponadto, że te warunki warunkowe w odpowiedzi Byte56 muszą być wywoływane przy każdej aktualizacji przez cały okres istnienia twojego programu i mogą być zagnieżdżone lub mieć w sobie warunki warunkowe, co znacznie pogorszy sprawę. Wskaźniki funkcji / delegaci są właściwą drogą.
Inżynier

3

Dla pełności

(Właściwie nie polecam tego robić, ponieważ napisanie czegoś takiego jest dość proste if(!already_called), ale byłoby to „poprawne”.)

Nic dziwnego, że standard C ++ 11 z idiomizował dość trywialny problem wywołania funkcji raz i sprawił, że była bardzo wyraźna:

#include <mutex>

void Update() {
if(mousebuttonpressed) {
    static std::once_flag f;
    std::call_once(f, ClickTheThing);
}

}

Trzeba przyznać, że standardowe rozwiązanie jest nieco lepsze od trywialnego w obecności wątków, ponieważ wciąż gwarantuje, że zawsze dzieje się dokładnie jedno wywołanie, nigdy coś innego.

Jednak zwykle nie uruchamiasz wielowątkowej pętli zdarzeń i naciskasz przyciski kilku myszy jednocześnie, więc bezpieczeństwo wątków jest nieco zbędne w twoim przypadku.

W praktyce oznacza to, że oprócz bezpiecznej dla wątków inicjalizacji lokalnej wartości logicznej (która C ++ 11 i tak gwarantuje, że jest bezpieczna dla wątków), wersja standardowa musi również wykonać atomową operację test_set przed wywołaniem lub nie wywołaniem funkcja.

Jeśli jednak lubisz wyrażać się otwarcie, istnieje rozwiązanie.

EDYCJA:
Huh. Teraz, kiedy siedzę tutaj i wpatruję się w ten kod przez kilka minut, jestem prawie skłonny go polecić.
W rzeczywistości nie jest to tak głupie, jak mogłoby się początkowo wydawać, w rzeczywistości bardzo jasno i jednoznacznie przekazuje twoje zamiary ... prawdopodobnie lepiej ustrukturyzowane i bardziej czytelne niż jakiekolwiek inne rozwiązanie, które obejmuje takie ifczy inne.


2

Niektóre sugestie będą się różnić w zależności od architektury.

Utwórz clickThisThing () jako wskaźnik funkcji / wariant / DLL / so / etc ... i upewnij się, że jest on inicjalizowany do wymaganej funkcji, gdy instancja obiektu / komponentu / modułu / etc ...

W module obsługi po każdym naciśnięciu przycisku myszy wywołaj wskaźnik funkcji clickThisThing (), a następnie natychmiast zamień wskaźnik na inny wskaźnik funkcji NOP (brak operacji).

Nie ma potrzeby używania flag i dodatkowej logiki, aby ktoś mógł później spieprzyć, wystarczy zadzwonić i wymienić za każdym razem, a ponieważ jest to NOP, NOP nic nie robi i zostaje zastąpiony NOP.

Lub możesz użyć flagi i logiki, aby pominąć połączenie.

Lub możesz odłączyć funkcję Update () po wywołaniu, aby świat zewnętrzny zapomniał o tym i nigdy więcej go nie wywoływał.

Lub masz sam clickThisThing () używa jednej z tych koncepcji, aby odpowiedzieć użyteczną pracą tylko raz. W ten sposób możesz użyć bazowego obiektu / komponentu / modułu clickThisThingOnce () i utworzyć go w dowolnym miejscu, w którym potrzebujesz tego zachowania, a żaden z twoich programów aktualizujących nie potrzebuje specjalnej logiki w tym miejscu.


2

Użyj wskaźników funkcji lub delegatów.

Zmienna zawierająca wskaźnik funkcji nie musi być jawnie badana, dopóki nie trzeba wprowadzić zmiany. A kiedy już nie potrzebujesz, aby uruchomić tę logikę, zamień ją na odwołanie do funkcji pusta / brak operacji. Porównaj z wykonywaniem warunkowej kontroli każdej klatki przez całe życie programu - nawet jeśli ta flaga była potrzebna tylko w pierwszych kilku ramkach po uruchomieniu! - Nieczyste i nieefektywne. (Punkt Boreala na temat przewidywania jest jednak potwierdzony w tym względzie.)

Zagnieżdżone warunki warunkowe, które często stanowią, są nawet bardziej kosztowne niż konieczność sprawdzania jednego warunku w każdej ramce, ponieważ przewidywanie gałęzi nie może już wtedy wykonać swojej magii. Oznacza to regularne opóźnienia rzędu 10s cykli - przeciągnięcia rurociągów par excellence . Zagnieżdżanie może wystąpić powyżej lub poniżej tej flagi boolowskiej. Nie potrzebuję przypominać czytelnikom, jak szybko mogą stać się złożone pętle gier.

Z tego powodu istnieją wskaźniki funkcji - używaj ich!


1
Myślimy w ten sam sposób. Najskuteczniejszym sposobem byłoby odłączenie funkcji Update () od tego, kto nieustannie ją wywołuje po zakończeniu pracy, co jest bardzo powszechne w konfiguracjach sygnału + gniazda w stylu Qt. OP nie określił środowiska wykonawczego, więc jesteśmy zahamowani, jeśli chodzi o konkretne optymalizacje.
Patrick Hughes,

1

W niektórych językach skryptowych, takich jak javascript lub lua, można łatwo wykonać testowanie odwołania do funkcji. W Lua ( love2d ):

function ClickTheThing()
      .......
end
....

local ctt = ClickTheThing  --I make a local reference to function

.....

function update(dt)
..
    if ctt then
      ctt()
      ctt=nil
    end
..
end

0

W komputerze z architekturą Von Neumann w pamięci przechowywane są instrukcje i dane programu. Jeśli chcesz, aby kod był uruchamiany tylko raz, możesz mieć kod w metodzie, który zastąpi metodę NOP po jej zakończeniu. W ten sposób, jeśli metoda miałaby zostać uruchomiona ponownie, nic by się nie stało.


6
Spowoduje to uszkodzenie kilku architektur, które zapisują pamięć programu do ochrony lub działają z ROM. Prawdopodobnie uruchomi także losowe ostrzeżenia antywirusowe, w zależności od zainstalowanej instalacji.
Patrick Hughes

0

W python, jeśli planujesz być palantem dla innych osób w projekcie:

code = compile('main code'+'del code', '<string>', 'exec')
exec code

ten skrypt demonstruje kod:

from time import time as t

code = compile('del code', '<string>', 'exec')

print 'see code object:'
print code

while True:
    try:
        user = compile(raw_input('play with it (variable name is code) (type done to be done:\t'), '<user_input>', 'exec')
        exec user
    except BaseException as e:
        print e
        break

exec code

print 'no more code object:'
try:
    print code
except BaseException as e:
    print e

while True:
    try:
        user = compile(raw_input('try to play with it again (type done to be done:\t'), '<user_input>', 'exec')
        exec user
    except BaseException as e:
        print e
        break

0

Oto kolejny wkład, jest trochę bardziej ogólny / wielokrotnego użytku, nieco bardziej czytelny i łatwy do utrzymania, ale prawdopodobnie mniej wydajny niż inne rozwiązania.

Podsumujmy naszą jednorazową logikę w klasie Raz:

class Once
{
    private Action body;
    public bool HasBeenCalled { get; private set; }
    public Once(Action body)
    {
        this.body = body;
    }
    public void Call()
    {
        if (!HasBeenCalled)
        {
            body();
        }
        HasBeenCalled = true;
    }
    public void Reset() { HasBeenCalled = false; }
}

Używanie go jak w podanym przykładzie wygląda następująco:

Once clickTheThingOnce = new Once(ClickTheThing);

void Update() {
    if(mousebuttonpressed) {
        clickTheThingOnce.Call(); // ClickTheThing is only called once
                                  // or until .Reset() is called
    }
}

0

Możesz rozważyć użycie zmiennej jako zmiennej.

void doOnce()
{
   static bool executed = false;

   if(executed == false)
   {
      ...Your code here
      executed = true;
   }
}

Możesz to zrobić w ClickTheThing()samej funkcji lub utworzyć inną funkcję i wywołać ClickTheThing()w treści if.

Jest podobny do pomysłu używania flagi, jak sugerowano w innych rozwiązaniach, ale flaga jest chroniona przed innym kodem i nie można jej zmienić gdzie indziej, ponieważ jest lokalna dla funkcji.


1
... ale to uniemożliwia uruchomienie kodu raz dla każdej instancji obiektu. (Jeśli tego potrzebuje użytkownik ..;))
Vaillancourt

0

Ten dylemat spotkałem z wejściem kontrolera Xbox. Chociaż nie dokładnie tak samo, jest cholernie podobny. Możesz zmienić kod w moim przykładzie, aby dostosować go do swoich potrzeb.

Edycja: Twoja sytuacja wykorzystałaby to ->

https://docs.microsoft.com/en-us/windows/desktop/api/winuser/ns-winuser-tagrawmouse

I możesz dowiedzieć się, jak utworzyć surową klasę wejściową za pomocą ->

https://docs.microsoft.com/en-us/windows/desktop/inputdev/raw-input

Ale ... teraz na super niesamowity algorytm ... nie bardzo, ale hej .. to całkiem fajne :)

* Więc ... możemy przechowywać stany każdego przycisku, które są wciśnięte, zwolnione i wstrzymane !!! Możemy również sprawdzić czas wstrzymania, ale wymaga to pojedynczego wyciągu if i możemy sprawdzić dowolną liczbę przycisków, ale dla niektórych informacji można znaleźć poniższe zasady.

Oczywiście, jeśli chcemy sprawdzić, czy coś jest wciśnięte, zwolnione itp., Zrobilibyśmy „If (This) {}”, ale pokazuje to, w jaki sposób możemy uzyskać stan prasy, a następnie wyłączyć następną klatkę, aby… ismousepressed ”będzie faktycznie fałszywy przy następnym sprawdzeniu.

Pełny kod tutaj: https://github.com/JeremyDX/DX_B/blob/master/DX_B/XGameInput.cpp

Jak to działa..

Więc nie jestem pewien, jakie wartości otrzymujesz, gdy obrazujesz, czy przycisk jest wciśnięty, czy nie, ale w zasadzie, gdy ładuję w XInput, otrzymuję 16-bitową wartość między 0 a 65535, to ma 15 bitów możliwych stanów dla „wciśniętego”.

Problem występował za każdym razem, gdy to sprawdzałem. Po prostu dawał mi aktualny stan informacji. Potrzebowałem sposobu, aby przekonwertować bieżący stan na wartości wyciskane, zwolnione i wstrzymane.

Więc to, co zrobiłem, jest następujące.

Najpierw tworzymy zmienną „CURRENT”. Za każdym razem, gdy sprawdzamy te dane, ustawiamy „CURRENT” na zmienną „PREVIOUS”, a następnie przechowujemy nowe dane na „Current”, jak pokazano tutaj ->

uint64_t LAST = CURRENT;
CURRENT = gamepad.wButtons;

Dzięki tym informacjom jest to ekscytujące !!

Możemy teraz dowiedzieć się, czy przycisk jest ODŁĄCZONY!

BUTTONS_HOLD = LAST & CURRENT;

To robi w zasadzie porównuje dwie wartości i wszystkie naciśnięcia przycisków, które są pokazane w obu pozostaną 1, a wszystko inne ustawione na 0.

Tj. (1 | 2 | 4) i (2 | 4 | 8) da (2 | 4).

Teraz, kiedy już mamy, które przyciski są „HELD” w dół. Możemy zdobyć resztę.

Naciśnięcie jest proste. Przyjmujemy stan „AKTUALNY” i usuwamy wszystkie przytrzymane przyciski.

BUTTONS_PRESSED = CURRENT ^ BUTTONS_HOLD;

Wydany jest taki sam, tylko porównujemy go do naszego OSTATNEGO stanu.

BUTTONS_RELEASED = LAST ^ BUTTONS_HOLD;

Patrząc na sytuację z tłoczeniem. Jeśli powiedzmy, że obecnie mieliśmy 2 | 4 | 8 wciśnięty. Okazało się, że 2 | 4, gdzie odbyła się. Po usunięciu bitów trzymanych zostaje nam tylko 8. To nowo wciśnięty bit dla tego cyklu.

To samo można zastosować do wydania. W tym scenariuszu „OSTATNI” został ustawiony na 1 | 2 | 4. Więc kiedy usuniemy 2 | 4 bity. Pozostaje nam 1. Więc przycisk 1 został zwolniony od ostatniej klatki.

Powyższy scenariusz jest prawdopodobnie najbardziej idealną sytuacją, jaką możesz zaoferować do porównania bitów i zapewnia 3 poziomy danych bez instrukcji if lub pętli tylko 3 szybkie obliczenia bitów.

Chciałem również udokumentować dane wstrzymania, więc chociaż moja sytuacja nie jest idealna ... to w zasadzie ustawiamy ograniczenia, które chcemy sprawdzić.

Dlatego za każdym razem, gdy ustawiamy nasze dane Press / Release / Hold, sprawdzamy, czy dane hold nadal odpowiadają bieżącemu testowi bitów hold. Jeśli nie, resetujemy czas do aktualnego czasu. W moim przypadku ustawiam go na indeksy klatek, więc wiem, dla ilu klatek zostało wstrzymane.

Minusem tego podejścia jest to, że nie mogę uzyskać indywidualnych czasów wstrzymania, ale możesz sprawdzić wiele bitów jednocześnie. To znaczy, jeśli ustawię bit wstrzymania na 1 | 16, jeśli 1 lub 16 nie zostaną utrzymane, to się nie powiedzie. Wymaga to przytrzymania WSZYSTKICH tych przycisków, aby kontynuować tykanie.

Następnie, jeśli spojrzysz na kod, zobaczysz wszystkie fajne wywołania funkcji.

Twój przykład sprowadza się zatem do sprawdzenia, czy naciśnięcie przycisku miało miejsce, a naciśnięcie przycisku może wystąpić tylko raz przy użyciu tego algorytmu. Przy następnym sprawdzeniu, aby nacisnąć, nie będzie istnieć, ponieważ nie można nacisnąć więcej niż raz, aby zwolnić, zanim będzie można nacisnąć ponownie.


Więc w zasadzie używasz pola bitowego zamiast bool, jak podano w innych odpowiedziach?
Vaillancourt

Tak, prawie lol ..
Jeremy Trifilo

Może być użyty do jego wymagań, chociaż w poniższej strukturze msdn „usButtonFlags” zawiera pojedynczą wartość wszystkich stanów przycisków. docs.microsoft.com/en-us/windows/desktop/api/winuser/…
Jeremy Trifilo

Nie jestem jednak pewien, skąd bierze się konieczność powiedzenia tego wszystkiego o przyciskach kontrolera. Podstawowa treść odpowiedzi jest ukryta pod wieloma rozpraszającymi tekstami.
Vaillancourt

Tak, podałem tylko osobisty scenariusz i co zrobiłem, aby go zrealizować. Na pewno to posprzątam później i być może dodam dane z klawiatury i myszy i podejdę do tego.
Jeremy Trifilo

-3

Głupie tanie wyjście, ale możesz użyć pętli for

for (int i = 0; i < 1; i++)
{
    //code here
}

Kod w pętli będzie zawsze wykonywany po uruchomieniu pętli. Nic nie uniemożliwia jednorazowego wykonania kodu. Pętla lub brak pętli daje dokładnie ten sam wynik.
Vaillancourt
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.