¿Cómo me aseguro de que un código se ejecute solo una vez?


18

Tengo un código que solo quiero ejecutar una vez, aunque las circunstancias que desencadenan ese código podrían ocurrir varias veces.

Por ejemplo, cuando el usuario hace clic con el mouse, quiero hacer clic en la cosa:

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

Sin embargo, con este código, cada vez que hago clic con el mouse, se hace clic en la cosa. ¿Cómo puedo hacer que suceda solo una vez?


77
De hecho, encuentro esto un poco divertido, ya que no creo que caiga dentro de "problemas de programación específicos del juego". Pero nunca me gustó esa regla.
ClassicThunder

3
@ClassicThunder Sí, ciertamente está más en el lado de la programación general de las cosas. Vote para cerrar si lo desea, publiqué la pregunta más para que tengamos algo que señalar a las personas cuando hagan preguntas similares a esto. Puede ser abierto o cerrado para ese propósito.
MichaelHouse

Respuestas:


41

Usa una bandera booleana.

En el ejemplo que se muestra, modificaría el código para que se parezca a lo siguiente:

//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();
    }
}

Además, si desea poder repetir la acción, pero limite la frecuencia de la acción (es decir, el tiempo mínimo entre cada acción). Usaría un enfoque similar, pero restablecería la bandera después de un cierto período de tiempo. Vea mi respuesta aquí para obtener más ideas al respecto.


1
La respuesta obvia a su pregunta :-) Me interesaría ver enfoques alternativos si usted o alguien más conoce alguno. El uso de un booleano global para esto siempre ha sido maloliente para mí.
Evorlor

1
@Evorlor No es obvio para todos :). También estoy interesado en soluciones alternativas, ¡tal vez podamos aprender algo no tan obvio!
MichaelHouse

2
@Evorlor como alternativa, puede usar delegados (punteros de función) y cambiar la acción que se realiza. Por ejemplo, onStart() { delegate = doSomething; } ... if(mousebuttonpressed) { delegate.Execute(); }y void doSomething() { doStuff(); delegate = funcDoNothing; }al final, todas las opciones terminan teniendo una bandera de algún tipo que establezca / desarme ... y delegar en este caso no es otra cosa (excepto si tiene más de dos opciones, ¿qué hacer quizás?).
wondra

2
@wondra Sí, esa es una manera. Añádelo. También podríamos hacer una lista de posibles soluciones a esta pregunta.
MichaelHouse

1
@JamesSnell Es más probable si fuera de subprocesos múltiples. Pero sigue siendo una buena práctica.
MichaelHouse

21

Si el indicador bool no es suficiente o si desea mejorar la legibilidad * del código en el void Update()método, podría considerar el uso de delegados (punteros de función):

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(){}
}

Para simples "ejecutar una vez" los delegados son excesivos, por lo que sugeriría usar la bandera bool en su lugar.
Sin embargo, si necesita una funcionalidad más complicada, los delegados son probablemente la mejor opción. Por ejemplo, si desea encadenar, ejecute más acciones diferentes: una en el primer clic, otra en el segundo y una más en la tercera, simplemente puede hacer:

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

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

en lugar de plagar tu código con decenas de banderas diferentes.
* a costa de una menor mantenibilidad del resto del código


Hice algunos perfiles con resultados más o menos como esperaba:

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

La primera prueba se ejecutó en Unity 5.1.2, medido con System.Diagnostics.Stopwatchun proyecto integrado de 32 bits (¡no en el diseñador!). El otro en Visual Studio 2015 (v140) compilado en modo de lanzamiento de 32 bits con el indicador / Ox. Ambas pruebas se ejecutaron en la CPU Intel i5-4670K a 3.4GHz, con 10,000,000 de iteraciones para cada implementación. código:

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

conclusión: Si bien el compilador de Unity hace un buen trabajo al optimizar las llamadas a funciones, dando aproximadamente el mismo resultado tanto para el indicador positivo como para los delegados (21 y 25 ms respectivamente), la predicción errónea de la rama o la llamada a la función sigue siendo bastante costosa (nota: se debe asumir que el delegado debe tener caché en esta prueba).
Curiosamente, el compilador Unity no es lo suficientemente inteligente como para optimizar la rama cuando hay 99 millones de predicciones erróneas consecutivas, por lo que la negación manual de la prueba produce un cierto aumento del rendimiento que da el mejor resultado de 5 ms. La versión C ++ no muestra ningún aumento de rendimiento para negar la condición, sin embargo, la sobrecarga general de la llamada de función es significativamente menor.
Lo más importante: la diferencia es prácticamente irrelevante para cualquier escenario del mundo real


44
AFAIK esto también es mejor desde el punto de vista del rendimiento. Llamar a una función no operativa, suponiendo que esa función ya está en la memoria caché de instrucciones, es mucho más rápido que verificar marcas, especialmente cuando esa verificación está anidada bajo otras condiciones.
Ingeniero

2
Creo que Navin tiene razón, es probable que tenga un rendimiento peor que verificar la bandera booleana, a menos que su JIT sea realmente inteligente y reemplace toda la función con literalmente nada. Pero a menos que hagamos un perfil de los resultados, no podemos estar seguros.
wondra

2
Hmm, esta respuesta me recuerda la evolución de un ingeniero SW . Broma aparte, si realmente necesitas (y estás seguro) de este aumento de rendimiento, anímate. Si no, sugeriría mantenerlo BESO y usar una bandera booleana, como se propone en otra respuesta . Sin embargo, ¡+1 para la alternativa representada aquí!
mucaho

1
Evite los condicionales cuando sea posible. Estoy hablando de lo que sucede a nivel de máquina, ya sea su propio código nativo, un compilador JIT o un intérprete. El acierto de caché L1 es casi lo mismo que un acierto de registro, posiblemente un poco más lento, es decir, 1 o 2 ciclos, mientras que la predicción errónea de la rama, que ocurre con menos frecuencia pero aún demasiado en promedio, cuesta del orden de 10-20 ciclos. Vea la respuesta de Jalf: stackoverflow.com/questions/289405/… Y esto: stackoverflow.com/questions/11227809/…
Ingeniero

1
Además, recuerde que estos condicionales en la respuesta de Byte56 deben llamarse todas las actualizaciones durante la vida útil de su programa, y ​​pueden estar anidados o tener condicionales anidados dentro de ellos, lo que empeora las cosas. Los punteros / delegados de funciones son el camino a seguir.
Ingeniero

3

Por completitud

(En realidad, no recomiendo que hagas esto, ya que en realidad es bastante sencillo escribir algo así if(!already_called), pero sería "correcto" hacerlo).

Como era de esperar, el estándar C ++ 11 ha idiomizado el problema bastante trivial de llamar a una función una vez, y lo ha hecho súper explícito:

#include <mutex>

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

}

Es cierto que la solución estándar es algo superior a la trivial en presencia de hilos, ya que aún garantiza que siempre ocurra exactamente una llamada, nunca algo diferente.

Sin embargo, normalmente no está ejecutando un bucle de eventos multiproceso y presionando los botones de varios ratones al mismo tiempo, por lo que la seguridad del subproceso es un poco superflua para su caso.

En términos prácticos, significa que además de la inicialización segura para subprocesos de un booleano local (que C ++ 11 garantiza que es seguro para subprocesos de todos modos), la versión estándar también debe realizar una operación atómica test_set antes de llamar o no llamar la función.

Aún así, si te gusta ser explícito, ahí está la solución.

EDITAR:
Eh. Ahora que he estado sentado aquí, mirando ese código durante algunos minutos, estoy casi inclinado a recomendarlo.
En realidad, no es tan tonto como puede parecer al principio, de hecho, comunica de manera muy clara e inequívoca su intención ... posiblemente mejor estructurada y más legible que cualquier otra solución que implique uno ifo tal.


2

Algunas sugerencias variarán según la arquitectura.

Cree clickThisThing () como un puntero / variante de función / DLL / so / etc ... y asegúrese de que se inicialice a su función requerida cuando se instancia el objeto / componente / módulo / etc ...

En el controlador cada vez que se presiona el botón del mouse, llame al puntero de función clickThisThing () y luego reemplace inmediatamente el puntero con otro puntero de función NOP (sin operación).

No hay necesidad de banderas y lógica adicional para que alguien se equivoque más tarde, solo llámelo y reemplácelo cada vez y dado que es un NOP, el NOP no hace nada y se reemplaza con un NOP.

O puede usar la bandera y la lógica para omitir la llamada.

O puede desconectar la función Update () después de la llamada para que el mundo exterior se olvide de ella y nunca la vuelva a llamar.

O puede hacer que el clickThisThing () use uno de estos conceptos para responder solo con trabajo útil una vez. De esta manera, puede usar un objeto / componente / módulo clickThisThingOnce () de línea de base e instanciarlo en cualquier lugar donde necesite este comportamiento y ninguno de sus actualizadores necesita una lógica especial en todo el lugar.


2

Utilice punteros de función o delegados.

La variable que contiene el puntero de función no necesita ser examinada explícitamente hasta que se necesite hacer un cambio. Y cuando ya no necesite dicha lógica para ejecutar, reemplace con una referencia a una función vacía / no operativa. Compare con hacer verificaciones condicionales en cada cuadro durante toda la vida de su programa, ¡incluso si ese indicador solo fuera necesario en los primeros cuadros después del inicio! - Impuro e ineficiente. (Sin embargo, el punto de Boreal sobre la predicción se reconoce a este respecto).

Los condicionales anidados, que a menudo constituyen, son aún más costosos que tener que verificar un solo condicional en cada cuadro, ya que la predicción de rama ya no puede hacer su magia en ese caso. Eso significa retrasos regulares en el orden de 10s de ciclos: puestos de ductos por excelencia . El anidamiento puede ocurrir arriba o debajo de esta bandera booleana. No necesito recordarle a los lectores cuán complejos pueden ser los bucles de juego rápidamente.

Los punteros de función existen por este motivo. ¡Úselos!


1
Estamos pensando en la misma línea. La forma más eficiente sería desconectar la cosa Update () de quien siga llamándolo sin descanso una vez que haya hecho el trabajo, lo cual es muy común en las configuraciones de señal + ranura de estilo Qt. El OP no especificó el entorno de tiempo de ejecución, por lo que estamos limitados cuando se trata de optimizaciones específicas.
Patrick Hughes

1

En algunos lenguajes de script como javascript o lua se puede hacer fácilmente probando la referencia de función. En 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

En una computadora Von Neumann Architecture , la memoria es donde se almacenan las instrucciones y los datos de su programa. Si desea que el código se ejecute solo una vez, puede hacer que el código del método sobrescriba el método con NOP una vez que el método esté completo. De esta manera, si el método se volviera a ejecutar, no pasaría nada.


66
Esto romperá algunas arquitecturas que escriben protegen la memoria del programa o se ejecutan desde la ROM. Posiblemente también activará advertencias antivirus al azar, dependiendo de lo que esté instalado.
Patrick Hughes

0

En python si planeas ser un imbécil con otras personas en el proyecto:

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

Este script demonestra el código:

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

Aquí hay otra contribución, es un poco más general / reutilizable, un poco más legible y mantenible, pero probablemente menos eficiente que otras soluciones.

Encapsulemos nuestra lógica de ejecución única en una clase, Una vez:

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; }
}

Usarlo como el ejemplo proporcionado se ve de la siguiente manera:

Once clickTheThingOnce = new Once(ClickTheThing);

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

0

Puede considerar usar static una variable.

void doOnce()
{
   static bool executed = false;

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

Puede hacer esto en la ClickTheThing()función misma, o crear otra función y llamar ClickTheThing()al cuerpo if.

Es similar a la idea de usar un indicador como se sugiere en otras soluciones, pero el indicador está protegido de otro código y no puede modificarse en ningún otro lugar debido a que es local para la función.


1
... pero esto evita que el código se ejecute una vez 'por instancia de objeto'. (Si eso es lo que necesita el usuario ...;))
Vaillancourt

0

De hecho, me encontré con este dilema con la entrada del controlador Xbox. Aunque no es exactamente lo mismo, es muy similar. Puede cambiar el código en mi ejemplo para satisfacer sus necesidades.

Editar: su situación usaría esto ->

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

Y puede aprender cómo crear una clase de entrada sin formato a través de ->

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

Pero ... ahora en el algoritmo súper impresionante ... no realmente, pero bueno ... es bastante genial :)

* Entonces ... ¡podemos almacenar los estados de cada botón y cuáles son presionados, liberados y retenidos! También podemos verificar el Tiempo de retención, pero eso requiere una sola declaración if y puede verificar cualquier número de botones, pero hay algunas reglas que se encuentran a continuación para obtener esta información.

Obviamente, si queremos comprobar si se presiona, suelta, etc., debe hacer "If (This) {}", pero esto muestra cómo podemos obtener el estado de la prensa y luego desactivarlo en el siguiente cuadro para que su " ismousepressed "en realidad será falso la próxima vez que verifique.

Código completo aquí: https://github.com/JeremyDX/DX_B/blob/master/DX_B/XGameInput.cpp

Cómo funciona..

Por lo tanto, no estoy seguro de los valores que recibe cuando describe si se presiona un botón o no, pero básicamente cuando cargo en XInput obtengo un valor de 16 bits entre 0 y 65535, esto tiene 15 bits de estados posibles para "Presionado".

El problema era que cada vez que revisaba esto simplemente me daría el estado actual de la información. Necesitaba una forma de convertir el estado actual en valores presionados, liberados y retenidos.

Entonces, lo que hice es lo siguiente.

Primero creamos una variable "ACTUAL". Cada vez que verificamos estos datos, establecemos "ACTUAL" en una variable "ANTERIOR" y luego almacenamos los nuevos datos en "Actual" como se ve aquí ->

uint64_t LAST = CURRENT;
CURRENT = gamepad.wButtons;

¡Con esta información, aquí es donde se pone emocionante!

¡Ahora podemos averiguar si un botón se está DETENIENDO!

BUTTONS_HOLD = LAST & CURRENT;

Lo que esto hace es básicamente que compara los dos valores y cualquier pulsación de botón que se muestra en ambos permanecerá 1 y todo lo demás se establecerá en 0.

Es decir (1 | 2 | 4) y (2 | 4 | 8) rendirán (2 | 4).

Ahora que tenemos qué botones están "HELD" hacia abajo. Podemos obtener el resto.

Presionado es simple ... tomamos nuestro estado "ACTUAL" y eliminamos cualquier botón presionado.

BUTTONS_PRESSED = CURRENT ^ BUTTONS_HOLD;

Lanzado es el mismo, solo que lo comparamos con nuestro ÚLTIMO estado.

BUTTONS_RELEASED = LAST ^ BUTTONS_HOLD;

Entonces mirando la situación presionada. Si digamos que actualmente teníamos 2 | 4 | 8 presionado. Encontramos que 2 | 4 donde se celebró. Cuando eliminamos los bits retenidos, solo nos queda 8. Este es el bit recién presionado para este ciclo.

Lo mismo se puede aplicar para Liberado. En este escenario, "ÚLTIMO" se estableció en 1 | 2 | 4. Entonces, cuando quitamos el 2 | 4 bits Nos quedamos con 1. Entonces, el botón 1 se soltó desde el último fotograma.

Este escenario anterior es probablemente la situación más ideal que puede ofrecer para la comparación de bits y proporciona 3 niveles de datos sin sentencias if o para bucles, solo 3 cálculos rápidos de bits.

También quería documentar los datos retenidos, así que aunque mi situación no es perfecta ... lo que hace es básicamente establecer los holdbits que queremos verificar.

Por lo tanto, cada vez que configuramos nuestros datos de Prensa / Liberación / Retención, verificamos si los datos de retención aún son iguales a la verificación de bit de retención actual. Si no es así, restablecemos la hora actual. En mi caso, lo configuro en índices de cuadros para saber cuántos cuadros se han mantenido presionados.

La desventaja de este enfoque es que no puedo obtener tiempos de espera individuales, pero puede verificar varios bits a la vez. Es decir, si establezco el bit de retención en 1 | 16 si no se mantienen 1 o 16, esto fallará. Por lo tanto, se requiere que TODOS esos botones se mantengan presionados para continuar marcando.

Luego, si observa el código, verá todas las llamadas a funciones ordenadas.

Por lo tanto, su ejemplo se reduciría a simplemente verificar si se presionó un botón y solo se puede presionar un botón con este algoritmo. En la próxima comprobación para presionar, no existirá, ya que no puede presionar más de una vez que tendría que soltar antes de poder presionar nuevamente.


Entonces, ¿básicamente utiliza un campo de bits en lugar de un bool como se indica en las otras respuestas?
Vaillancourt

Sí bastante lol ..
Jeremy Trifilo

Puede usarse para sus requisitos, aunque con la estructura msdn a continuación, "usButtonFlags" contiene un solo valor de todos los estados de los botones. docs.microsoft.com/en-us/windows/desktop/api/winuser/…
Jeremy Trifilo

Sin embargo, no estoy seguro de dónde viene la necesidad de decir todo esto sobre los botones del controlador. El contenido central de la respuesta está oculto debajo de una gran cantidad de texto que distrae.
Vaillancourt

Sí, solo estaba dando un escenario personal relacionado y lo que hice para lograrlo. Sin embargo, lo limpiaré más tarde con seguridad y tal vez agregaré la entrada del teclado y el mouse y daré un enfoque al respecto.
Jeremy Trifilo

-3

Estúpida salida barata pero podrías usar un bucle for

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

El código en el bucle siempre se ejecutará cuando se ejecute el bucle. Nada allí impide que el código se ejecute una vez. El bucle o sin bucle da exactamente el mismo resultado.
Vaillancourt
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.