Як переконатися, що фрагмент коду працює лише один раз?


18

У мене є якийсь код, який я хочу запустити лише один раз, навіть незважаючи на те, що обставини, які викликають цей код, можуть траплятися кілька разів.

Наприклад, коли користувач клацає мишкою, я хочу натиснути річ:

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

Однак із цим кодом щоразу, коли я клацаю мишкою, річ клацається. Як я можу це зробити лише один раз?


7
Я справді вважаю це своєрідним смішним, оскільки я не думаю, що воно підпадає під "специфічні проблеми програмування". Але мені це правило ніколи не подобалося.
ClassicThunder

3
@ClassicThunder Так, це, звичайно, більше на загальній програмічній стороні речей. Голосуйте, щоб закрити, якщо вам подобається, я більше розмістив це питання, щоб ми мали на що звернути увагу людей, коли вони задають подібні запитання. Для цього він може бути відкритим або закритим.
MichaelHouse

Відповіді:


41

Використовуйте булевий прапор.

У наведеному прикладі ви можете змінити код таким чином, як:

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

Далі, якщо ви хочете мати можливість повторити дію, але обмежте частоту дії (тобто мінімальний час між кожною дією). Ви б застосували подібний підхід, але скиньте прапор через певний час. Дивіться мою відповідь тут, щоб отримати більше ідей щодо цього.


1
Очевидна відповідь на ваше запитання :-) Мені буде цікаво побачити альтернативні підходи, якщо ви чи хтось інший знаєте про будь-який. Таке використання глобального булевого для мене завжди було смердючим.
Еворлор

1
@Evorlor Не очевидно для всіх :). Мене цікавлять і альтернативні рішення, можливо, ми зможемо навчитися чомусь не такому очевидному!
MichaelHouse

2
@Evorlor як альтернатива, ви можете використовувати делегати (покажчики функцій) і змінювати, що буде зроблено. наприклад , onStart() { delegate = doSomething; } ... if(mousebuttonpressed) { delegate.Execute(); }і , void doSomething() { doStuff(); delegate = funcDoNothing; }але в кінці кінців всі варіанти в кінцевому підсумку з прапором деяких видів ви встановили / невстановлені ... і делегатом в даному випадку не що інше , ( за винятком , якщо у вас є більше двох варіантів , що робити , може бути?).
wondra

2
@wondra Так, це спосіб. Додайте його. Ми можемо також скласти список можливих рішень з цього питання.
MichaelHouse

1
@JamesSnell Більш імовірно, якщо він був багатопоточним. Але все ж хороша практика.
MichaelHouse

21

Якщо прапор bool не вистачає або ви хочете покращити читабельність * коду в void Update()методі, ви можете розглянути можливість використання делегатів (покажчиків функцій):

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

Для простого "Execute Once" делегати є надмірними, тому я б запропонував використати прапор bool.
Однак якщо вам потрібен складніший функціонал, делегати, мабуть, кращий вибір. Наприклад, якщо ви хотіли виконати більше різних дій: один на перший клік, інший на другий і ще один на третій, ви можете просто зробити:

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

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

замість того, щоб кидати свій код десятками різних прапорів.
* за вартістю меншої ремонтоздатності решти коду


Я зробив кілька профілювання з результатами майже так, як я очікував:

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

Перший тест проводився на Unity 5.1.2, вимірювану System.Diagnostics.Stopwatchза 32-розрядним вбудованим проектом (не в дизайнері!). Інший на Visual Studio 2015 (v140), складений у 32-бітному режимі випуску з прапором / Ox. Обидва тести були виконані на процесорі Intel i5-4670K при 3,4 ГГц, з 10 000 000 ітерацій для кожної реалізації. код:

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

висновок: Хоча компілятор Unity добре справляється з оптимізацією викликів функцій, даючи приблизно однаковий результат як для позитивного прапора, так і для делегатів (21 і 25 мс відповідно), неправильне передбачення або виклик функції все ще досить дороге (зауважте: делегат повинен вважати кеш у цьому тесті).
Цікаво, що компілятор Unity недостатньо розумний, щоб оптимізувати галузь, коли є 99 мільйонів послідовних непередбачуваних прогнозів, тому ручне відкидання тесту дає певне підвищення продуктивності, що дає найкращий результат у 5 мс. Версія C ++ не демонструє підвищення продуктивності для відмови, але загальний накладний набір функціональних викликів значно нижчий.
найголовніше: різницю в значній мірі іррелевати для будь-якого сценарію реального світу


4
AFAIK це також краще з точки зору продуктивності. Виклик функції без відключення, якщо припустити, що функція вже знаходиться в кеш-пам'яті інструкцій, набагато швидше, ніж перевірка прапорів, особливо там, де ця перевірка вкладена в інших умовах.
Інженер

2
Я думаю, що Навін має рацію, я, швидше за все, матимуть гірші показники, ніж перевірка булевого прапора - якщо тільки ваш JIT не справді розумний і не замінить всю функцію буквально нічим. Але якщо ми не проаналізуємо результати, ми не можемо точно знати.
wondra

2
Гм, ця відповідь нагадує мені еволюцію інженера SW . Жартуйте вбік, якщо вам абсолютно потрібно (і ви впевнені) у цьому підвищення продуктивності, продовжуйте це робити. Якщо ні, то я б запропонував зберегти його KISS і використовувати булівський прапор, як запропоновано в іншій відповіді . Однак +1 для альтернативи, представленої тут!
mucaho

1
По можливості уникайте умовних умов. Я говорю про те, що відбувається на рівні машини, будь то ваш власний кодовий код, компілятор JIT або інтерпретатор. Хіт кешу L1 приблизно такий самий, як звернення до регістру, можливо, трохи повільніше, тобто 1 або 2 цикли, тоді як неправильне прогнозування гілок, що трапляється рідше, але все ж занадто багато в середньому, коштує на порядку 10-20 циклів. Знайомства Jalf у відповідь: stackoverflow.com/questions/289405 / ... А це: stackoverflow.com/questions/11227809 / ...
інженер

1
Крім того, пам’ятайте, що ці умови у відповіді Byte56 повинні викликатись кожним оновленням впродовж життя вашої програми, і вони цілком можуть бути вкладеними або вкладеними в них умовами, що робить питання набагато гіршими. Покажчики функцій / Делегати - це шлях.
Інженер

3

Для повноти

(Насправді не рекомендую робити це, оскільки насправді досить просто написати щось на кшталт if(!already_called), але це було б "правильно".)

Не дивно, що стандарт C ++ 11 визначив досить тривіальну проблему виклику функції один раз і зробив це супер явним:

#include <mutex>

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

}

Щоправда, стандартне рішення дещо перевершує тривіальне за наявністю потоків, оскільки воно все ще гарантує, що завжди відбувається точно один виклик, а не щось інше.

Однак ви, як правило, не працюєте з багатопотоковим циклом подій та натискаєте кнопки декількох мишей одночасно, так що безпека нитки є трохи зайвою для вашого випадку.

На практиці це означає, що на додаток до безпечної потокової ініціалізації локального булевого сигналу (що C ++ 11 гарантує безпеку для потоків), стандартна версія повинна також виконувати атомний тестовий набір до виклику чи не виклику функція.

І все-таки, якщо вам подобається явне, є рішення.

РЕДАКТ:
Ага. Тепер, коли я сиджу тут, дивлячись на цей код кілька хвилин, я майже схильний його рекомендувати.
Насправді це не так вже й нерозумно, як може здатися спочатку, насправді він дуже чітко і однозначно передає ваш намір ... мабуть, краще структурований і легше для читання, ніж будь-яке інше рішення, яке передбачає те ifчи інше .


2

Деякі пропозиції залежать від архітектури.

Створіть clickThisThing () як покажчик / варіант / DLL / так / тощо ... та переконайтесь, що він ініціалізується до потрібної функції, коли об'єкт / компонент / модуль / тощо ... інстанціюється.

У обробнику щоразу, коли натискається кнопка миші, тоді зателефонуйте вказівник функції clickThisThing (), а потім негайно замініть вказівник іншим вказівником функції NOP (без операції).

Немає необхідності у прапорах та додатковій логіці, щоб хтось згодом викрутив, просто зателефонуйте та замінюйте його кожен раз, а оскільки це НОП, НОП нічого не робить і замінюється на НОП.

Або ви можете використовувати прапор і логіку, щоб пропустити виклик.

Або ви можете відключити функцію Update () після виклику, щоб зовнішній світ забув про неї і більше ніколи не дзвонить.

Або ви самі маєте clickThisThing (), використовуючи одну з цих концепцій, щоб відповісти корисною роботою лише один раз. Таким чином, ви можете використовувати базовий об'єкт / компонент / модуль clickThisThingOnce () та інстанціювати його будь-де, де вам потрібна така поведінка, і жодному з ваших оновників не потрібна спеціальна логіка.


2

Використовуйте функціональні вказівники або делегати.

Змінна, що містить покажчик функції, не повинна бути явно досліджена, поки не потрібно внести зміни. І коли вам більше не потрібна зазначена логіка для запуску, замініть її на функцію порожній / без опції. Порівняйте з виконанням умовних умов перевіряє кожен кадр протягом усього життя вашої програми - навіть якщо цей прапор був потрібний лише в перші кілька кадрів після запуску! - Нечисті та неефективні. (Однак у цьому відношенні визнається думка Бореаля щодо прогнозування.)

Вкладені умовні умови, які вони часто складають, коштують навіть дорожче, ніж перевіряти один єдиний умовний кожен кадр, оскільки передбачення гілок вже не може виконати свою магію в такому випадку. Це означає регулярні затримки на порядку 10-ти циклів - трубопровідні стоянки на відмінності . Гніздування може відбуватися над цим булевим прапором або під ним. Навряд чи потрібно нагадувати читачам про те, як складні петлі гри можуть швидко стати.

З цієї причини існують покажчики функцій - використовуйте їх!


1
Ми думаємо по одній лінії. Найефективнішим способом було б від'єднати річ Update () від того, хто продовжує невпинно її називати, як тільки буде виконано роботу, що дуже часто зустрічається в сигналі Qt-стилю та налаштуваннях слотів. ОП не вказала середовище виконання, тому ми зупиняємось на конкретних оптимізаціях.
Патрік Х'юз

1

У деяких мовах сценаріїв, таких як javascript або 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

На комп'ютері Von Neumann Architecture пам'ять - це місце, де зберігаються вказівки та дані вашої програми. Якщо ви хочете змусити код запускатись лише один раз, ви можете мати код у методі перезаписати метод на NOP після закінчення методу. Таким чином, якби метод запустився знову, нічого не трапилось би.


6
Це порушить кілька архітектур, які записують пам'ять програми або запускаються з ПЗУ. Можливо, це також відкладе випадкові попередження антивірусу, залежно від того, що встановлено.
Патрік Х'юз

0

У python, якщо ви плануєте бути придурком для інших людей на проекті:

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

цей скрипт демонструє код:

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

Ось ще один внесок: він трохи більш загальний / багаторазовий, трохи легше читабельний та ремонтопридатний, але, ймовірно, менш ефективний, ніж інші рішення.

Давайте інкапсулюємо нашу колись виконувану логіку в класі, Раз:

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

Використовуючи його, як поданий приклад, виглядає так:

Once clickTheThingOnce = new Once(ClickTheThing);

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

0

Ви можете розглянути можливість використання статичної змінної.

void doOnce()
{
   static bool executed = false;

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

Ви можете це зробити в ClickTheThing()самій функції, або створити іншу функцію та зателефонувати ClickTheThing()в тіло if.

Це схоже на ідею використання прапора, як пропонується в інших рішеннях, але прапор захищений від іншого коду і не може бути змінений в іншому місці через локальну функцію.


1
... але це заважає вам один раз запускати код на "екземпляр об'єкта". (Якщо користувачеві це потрібно ..;))
Vaillancourt

0

Я насправді натрапив на цю дилему з введенням контролера Xbox. Хоча НЕ ТОЧНО те саме, це досить чортово схоже. Ви можете змінити код у моєму прикладі відповідно до ваших потреб.

Редагувати: Ваша ситуація використовує це ->

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

І ви можете дізнатися, як створити необроблений клас введення через ->

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

Але .. тепер на супер дивовижний алгоритм ... не дуже, але ей .. це дуже круто :)

* Отже ... ми можемо зберігати стани кожної кнопки, які натиснуті, випущені та затримані !!! Ми також можемо перевірити час утримування, але для цього потрібна одинарна операція if і може перевірити будь-яку кількість кнопок, але деякі правила див. Нижче для цієї інформації.

Очевидно, якщо ми хочемо перевірити, чи щось натиснуто, відпущено тощо. Ви зробите "If (This) {}", але це показує, як ми можемо отримати стан друку, а потім вимкнути його з наступного кадру, щоб ваш " ismousepress "насправді буде помилковим наступного разу, коли ви перевірите.

Повний код тут: https://github.com/JeremyDX/DX_B/blob/master/DX_B/XGameInput.cpp

Як це працює..

Тому я не впевнений, що значення, які ви отримуєте при зображенні, натиснута чи ні кнопка, але в основному, коли я завантажуюся в XInput, я отримую 16-бітне значення між 0 і 65535, це має 15 біт можливих станів для "Пресовано".

Проблема була щоразу, коли я перевіряв це, просто дав би мені поточний стан інформації. Мені знадобився спосіб перетворити поточний стан у значення "Пресовані", "Випущені" та "Утримувані".

Тому я зробив наступне.

Спочатку ми створюємо змінну "СУЧАСНА". Кожен раз, коли ми перевіряємо ці дані, ми встановлюємо "СУЧАСНІ" на змінну "ПОПЕРЕДНЯ", а потім зберігаємо нові дані в "Поточні", як показано тут ->

uint64_t LAST = CURRENT;
CURRENT = gamepad.wButtons;

З цією інформацією ось де це стає захоплюючим !!

Тепер ми можемо з'ясувати, чи кнопка ведеться вниз!

BUTTONS_HOLD = LAST & CURRENT;

Це, в основному, це порівняння двох значень і будь-яке натискання кнопки, яке показане в обох, буде 1 і все інше встановлено на 0.

Тобто (1 | 2 | 4) & (2 | 4 | 8) вийде (2 | 4).

Тепер, коли ми маємо, які кнопки "ДОПОМОЖЕНІ" вниз. Решту ми можемо отримати.

Натискання просте. Ми приймаємо стан "СУЧАСНО" і видаляємо всі утримувані кнопки.

BUTTONS_PRESSED = CURRENT ^ BUTTONS_HOLD;

Випущений - це те саме, що ми порівняємо його з НАЙ ОСТАННІм станом.

BUTTONS_RELEASED = LAST ^ BUTTONS_HOLD;

Тож дивлячись на ситуацію з пресованістю. Скажімо, на даний момент у нас було 2 | 4 | 8 натиснуті. Ми виявили, що 2 | 4, де проводиться. Коли ми видаляємо Held Bits, у нас залишається лише 8. Це нещодавно натиснутий біт для цього циклу.

Те саме можна застосувати і до випуску. У цьому сценарії "LAST" було встановлено на 1 | 2 | 4. Отже, коли ми видаляємо 2 | 4 біти. Нам залишається 1. Отже, кнопка 1 була відпущена з останнього кадру.

Наведений вище сценарій, мабуть, є найбільш ідеальною ситуацією, яку ви можете запропонувати для порівняння бітів, і він пропонує 3 рівні даних, без операторів, а для циклів - лише 3 швидкі розрядні обчислення.

Я також хотів задокументувати дані про затримку, тому хоча моя ситуація не є ідеальною ... що це робиться, ми в основному встановлюємо хобі, які ми хочемо перевірити.

Таким чином, кожного разу, коли ми встановлюємо наші дані про прес / реліз / утримування, ми перевіряємо, чи дані утримування досі дорівнюють поточній біті перевірки. Якщо це не так, ми скидаємо час до поточного часу. У моєму випадку я встановлюю його на індекси кадру, тому я знаю, скільки кадрів утримується.

Мінусом цього підходу є те, що я не можу отримати окремі рази утримування, але ви можете перевірити кілька бітів одночасно. Тобто, якщо я встановив біт утримування на 1 | 16 якщо 1 або 16 не відбудеться, це не вдасться. Тому потрібно продовжити натискання УСІХ цих кнопок.

Тоді якщо ви заглянете в код, ви побачите всі акуратні дзвінки функції.

Тож ваш приклад зводиться до простої перевірки, чи відбулося натискання кнопки та натискання кнопки може відбутися лише один раз за допомогою цього алгоритму. Під час наступної перевірки натиснути її не буде, оскільки ви не можете натиснути більше, як тільки вам доведеться відпустити, перш ніж ви зможете натиснути ще раз.


Отже, ви в основному використовуєте бітфілд замість bool, як зазначено в інших відповідях?
Vaillancourt

Так, симпатичне ..
Джеремі Трифіло

Він може бути використаний для його вимог, хоча в нижченаведеній msdn структурі "usButtonFlags" містить одне значення всіх станів кнопки. docs.microsoft.com/en-us/windows/desktop/api/winuser/…
Джеремі Трифіло

Я не впевнений, звідки виникає необхідність сказати все це про кнопки контролера. Основний зміст відповіді приховано під безліччю відволікаючих текстів.
Vaillancourt

Так, я просто давав пов'язаний особистий сценарій і те, що робив для його виконання. Я прибираю його пізніше, хоча точно, і, можливо, я додам вкладку клавіатури та миші та підходжу до цього.
Джеремі Трифіло

-3

Дурний дешевий вихід, але ви можете використовувати петлю

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

Код у циклі завжди буде виконуватися при виконанні циклу. Ніщо там не заважає виконувати код один раз. Цикл або відсутність циклу дає точно такий же результат.
Vaillancourt
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.