Rany, Brian, żałuję, że nie spotkałem się z twoim pytaniem wcześniej. Ponieważ jest to właściwie mój „wynalazek” (na dobre lub na złe), być może będę w stanie pomóc.
Wstawione: Najkrótszym możliwym wyjaśnieniem, jakie mogę zrobić, jest to, że jeśli normalne wykonanie jest jak wyrzucenie piłki w powietrze i złapanie jej, to wykonanie różnicowe jest jak żonglowanie.
Wyjaśnienie @ windfinder różni się od mojego i to jest OK. Ta technika nie jest łatwa do ogarnięcia głowy i zajęło mi jakieś 20 lat (z przerwami), aby znaleźć wyjaśnienia, które działają. Pozwólcie, że powtórzę tutaj:
Wszyscy rozumiemy prostą ideę, że komputer przechodzi przez program, bierze rozgałęzienia warunkowe na podstawie danych wejściowych i robi różne rzeczy. (Załóżmy, że mamy do czynienia tylko z prostym, strukturalnym kodem goto-bez i bez powrotu). Ten kod zawiera sekwencje instrukcji, podstawowe warunki warunkowe, proste pętle i wywołania podprogramów. (Zapomnij o funkcjach zwracających wartości na razie.)
Teraz wyobraź sobie, że dwa komputery wykonują ten sam kod krok po kroku i potrafią porównać notatki. Komputer 1 działa z danymi wejściowymi A, a komputer 2 - z danymi wejściowymi B. Działają one krok po kroku obok siebie. Jeśli dojdą do warunkowej instrukcji, takiej jak IF (test) .... ENDIF, i jeśli mają różnicę zdań co do tego, czy test jest prawdziwy, to ten, który mówi, że test jest fałszywy, przeskakuje do ENDIF i czeka na jego siostra, aby nadrobić zaległości. (Dlatego kod ma taką samą strukturę, że wiemy, że siostra w końcu dotrze do ENDIF).
Ponieważ oba komputery mogą ze sobą rozmawiać, mogą porównywać notatki i szczegółowo wyjaśniać, w jaki sposób te dwa zestawy danych wejściowych i historie wykonania różnią się.
Oczywiście w wykonaniu różnicowym (DE) odbywa się to za pomocą jednego komputera, symulując dwa.
TERAZ, załóżmy, że masz tylko jeden zestaw danych wejściowych, ale chcesz zobaczyć, jak zmienił się od czasu 1 do czasu 2. Załóżmy, że program, który wykonujesz, jest serializatorem / deserializatorem. Podczas wykonywania zarówno serializujesz (zapisujesz) bieżące dane, jak i deserializujesz (wczytujesz) poprzednie dane (które zostały zapisane ostatnim razem, gdy to zrobiłeś). Teraz możesz łatwo zobaczyć, jakie są różnice między tym, jakie dane były ostatnio, a tym, jakie są tym razem.
Plik, do którego piszesz, i stary plik, z którego czytasz, razem tworzą kolejkę lub FIFO (pierwszy na wejściu, pierwszy na wyjściu), ale nie jest to bardzo głęboka koncepcja.
Przyszło mi do głowy, gdy pracowałem nad projektem graficznym, w którym użytkownik mógł konstruować małe procedury procesora wyświetlacza zwane „symbolami”, które można było łączyć w większe procedury, aby malować takie rzeczy, jak schematy rur, zbiorników, zaworów, itp. Chcieliśmy, aby diagramy były „dynamiczne” w tym sensie, że mogły się stopniowo aktualizować bez konieczności przerysowywania całego diagramu. (Sprzęt był powolny według dzisiejszych standardów.) Zdałem sobie sprawę, że (na przykład) procedura rysowania słupka wykresu słupkowego może zapamiętać jego starą wysokość i po prostu stopniowo się aktualizować.
To brzmi jak OOP, prawda? Jednak zamiast „uczynić” „obiekt”, mogłem skorzystać z przewidywalności sekwencji wykonania procedury diagramu. Mógłbym zapisać wysokość paska w sekwencyjnym strumieniu bajtów. Następnie, aby zaktualizować obraz, mógłbym po prostu uruchomić procedurę w trybie, w którym sekwencyjnie odczytuje swoje stare parametry, podczas gdy zapisuje nowe parametry, aby być gotowym do następnego przebiegu aktualizacji.
Wydaje się to głupio oczywiste i wydaje się zepsuć, gdy tylko procedura zawiera warunek, ponieważ wtedy nowy i stary strumień stracą synchronizację. Ale potem dotarło do mnie, że gdyby również zserializowali wartość logiczną testu warunkowego, mogliby wrócić do synchronizacji . Przekonanie się, a potem udowodnienie, że to zawsze zadziała, pod warunkiem przestrzegania prostej zasady („zasada trybu kasowania”) zajęło trochę czasu .
W rezultacie użytkownik może zaprojektować te „dynamiczne symbole” i złożyć je w większe diagramy, bez martwienia się o to, jak będą dynamicznie aktualizować, bez względu na to, jak skomplikowany lub strukturalnie zmienny byłby wyświetlacz.
W tamtych czasach musiałem się martwić o interferencję między obiektami wizualnymi, aby wymazanie jednego nie zaszkodziło innym. Jednak teraz używam tej techniki z kontrolkami systemu Windows i pozwalam systemowi Windows zająć się problemami z renderowaniem.
Więc co to osiąga? Oznacza to, że mogę zbudować okno dialogowe, pisząc procedurę malowania elementów sterujących, i nie muszę się martwić o faktyczne zapamiętywanie obiektów kontrolnych lub zajmowanie się ich przyrostową aktualizacją lub sprawianiem, że pojawiają się / znikają / poruszają się zgodnie z warunkami. Rezultatem jest znacznie mniejszy i prostszy kod źródłowy okna dialogowego, o mniej więcej rząd wielkości, a rzeczy takie jak układ dynamiczny lub zmiana liczby kontrolek lub posiadanie tablic lub siatek kontrolek są trywialne. Ponadto formant, taki jak pole edycji, może być w trywialny sposób powiązany z danymi aplikacji, które edytuje, i zawsze będzie można udowodnić poprawność, a ja nigdy nie będę musiał zajmować się jego zdarzeniami. Wprowadzenie pola edycyjnego dla zmiennej ciągu aplikacji jest edycją jednowierszową.
- Dlaczego trudno to zrozumieć?
Najtrudniej było mi wyjaśnić, że wymaga to innego myślenia o oprogramowaniu. Programiści są tak mocno przywiązani do poglądu oprogramowania na obiekt-działanie, że chcą wiedzieć, jakie są obiekty, jakie są klasy, jak „budują” wyświetlacz i jak radzą sobie ze zdarzeniami. bomba, żeby ich z niej wysadzić. To, co staram się przekazać, to to, co naprawdę się liczy to, co musisz powiedzieć?Wyobraź sobie, że tworzysz język specyficzny dla domeny (DSL), w którym wszystko, co musisz zrobić, to powiedzieć „Chcę edytować zmienną A tutaj, zmienną B tam, a zmienną C tam”, a on w magiczny sposób zajmie się tym za Ciebie . Na przykład w Win32 istnieje ten „język zasobów” do definiowania okien dialogowych. To doskonale DSL, z wyjątkiem tego, że nie idzie wystarczająco daleko. Nie „żyje” w głównym języku proceduralnym, nie obsługuje zdarzeń za Ciebie, ani nie zawiera pętli / warunków / podprogramów. Ale to dobrze, a Dynamic Dialogs próbuje dokończyć robotę.
Zatem inny sposób myślenia jest następujący: aby napisać program, najpierw należy znaleźć (lub wymyślić) odpowiedni DSL i zakodować w nim jak najwięcej swojego programu. Niech to sobie z wszystkich obiektów i działań, które istnieją tylko dla dobra implementacja jest.
Jeśli chcesz naprawdę zrozumieć wykonywanie różnicowe i używać go, jest kilka trudnych problemów, które mogą Cię potknąć. Kiedyś zakodowałem to w makrach Lispa , gdzie te trudne bity mogą być obsłużone za Ciebie, ale w "normalnych" językach wymaga to pewnej dyscypliny programisty, aby uniknąć pułapek.
Przepraszam, że jestem tak rozwlekły. Gdybym nie miał sensu, byłbym wdzięczny, gdybyś zwrócił na to uwagę, a ja mogę spróbować to naprawić.
Dodany:
W Java Swing istnieje przykładowy program o nazwie TextInputDemo. Jest to statyczne okno dialogowe, zajmujące 270 linii (nie licząc listy 50 stanów). W dynamicznych dialogach (w MFC) jest to około 60 linii:
#define NSTATE (sizeof(states)/sizeof(states[0]))
CString sStreet;
CString sCity;
int iState;
CString sZip;
CString sWholeAddress;
void SetAddress(){
CString sTemp = states[iState];
int len = sTemp.GetLength();
sWholeAddress.Format("%s\r\n%s %s %s", sStreet, sCity, sTemp.Mid(len-3, 2), sZip);
}
void ClearAddress(){
sWholeAddress = sStreet = sCity = sZip = "";
}
void CDDDemoDlg::deContentsTextInputDemo(){
int gy0 = P(gy);
P(www = Width()*2/3);
deStartHorizontal();
deStatic(100, 20, "Street Address:");
deEdit(www - 100, 20, &sStreet);
deEndHorizontal(20);
deStartHorizontal();
deStatic(100, 20, "City:");
deEdit(www - 100, 20, &sCity);
deEndHorizontal(20);
deStartHorizontal();
deStatic(100, 20, "State:");
deStatic(www - 100 - 20 - 20, 20, states[iState]);
if (deButton(20, 20, "<")){
iState = (iState+NSTATE - 1) % NSTATE;
DD_THROW;
}
if (deButton(20, 20, ">")){
iState = (iState+NSTATE + 1) % NSTATE;
DD_THROW;
}
deEndHorizontal(20);
deStartHorizontal();
deStatic(100, 20, "Zip:");
deEdit(www - 100, 20, &sZip);
deEndHorizontal(20);
deStartHorizontal();
P(gx += 100);
if (deButton((www-100)/2, 20, "Set Address")){
SetAddress();
DD_THROW;
}
if (deButton((www-100)/2, 20, "Clear Address")){
ClearAddress();
DD_THROW;
}
deEndHorizontal(20);
P((gx = www, gy = gy0));
deStatic(P(Width() - gx), 20*5, (sWholeAddress != "" ? sWholeAddress : "No address set."));
}
Dodany:
Oto przykładowy kod do edycji tablicy pacjentów szpitalnych w około 40 wierszach kodu. Linie 1-6 definiują „bazę danych”. Wiersze 10–23 określają ogólną zawartość interfejsu użytkownika. Wiersze 30–48 definiują elementy sterujące do edycji rekordu pojedynczego pacjenta. Zwróć uwagę, że forma programu prawie nie zwraca uwagi na zdarzenia w czasie, tak jakby wystarczyło tylko raz utworzyć wyświetlacz. Następnie, jeśli tematy są dodawane lub usuwane lub mają miejsce inne zmiany strukturalne, jest po prostu ponownie wykonywana, tak jakby była odtwarzana od zera, z wyjątkiem tego, że DE powoduje zamiast tego aktualizację przyrostową. Zaletą jest to, że programista nie musi poświęcać żadnej uwagi ani pisać żadnego kodu, aby wykonać przyrostowe aktualizacje interfejsu użytkownika i gwarantujemy ich poprawność. Mogłoby się wydawać, że to ponowne wykonanie byłoby problemem z wydajnością, ale tak nie jest,
1 class Patient {public:
2 String name;
3 double age;
4 bool smoker; // smoker only relevant if age >= 50
5 };
6 vector< Patient* > patients;
10 void deContents(){ int i;
11 // First, have a label
12 deLabel(200, 20, “Patient name, age, smoker:”);
13 // For each patient, have a row of controls
14 FOR(i=0, i<patients.Count(), i++)
15 deEditOnePatient( P( patients[i] ) );
16 END
17 // Have a button to add a patient
18 if (deButton(50, 20, “Add”)){
19 // When the button is clicked add the patient
20 patients.Add(new Patient);
21 DD_THROW;
22 }
23 }
30 void deEditOnePatient(Patient* p){
31 // Determine field widths
32 int w = (Width()-50)/3;
33 // Controls are laid out horizontally
34 deStartHorizontal();
35 // Have a button to remove this patient
36 if (deButton(50, 20, “Remove”)){
37 patients.Remove(p);
37 DD_THROW;
39 }
40 // Edit fields for name and age
41 deEdit(w, 20, P(&p->name));
42 deEdit(w, 20, P(&p->age));
43 // If age >= 50 have a checkbox for smoker boolean
44 IF(p->age >= 50)
45 deCheckBox(w, 20, “Smoker?”, P(&p->smoker));
46 END
47 deEndHorizontal(20);
48 }
Dodano: Brian zadał dobre pytanie i pomyślałem, że odpowiedź należy do głównego tekstu tutaj:
@Mike: Nie jestem pewien, co właściwie robi instrukcja „if (deButton (50, 20,„ Add ”)) {”. Co robi funkcja deButton? Czy twoje pętle FOR / END używają jakiegoś makra czy czegoś takiego? - Brian.
@Brian: Tak, instrukcje FOR / END i IF to makra. Projekt SourceForge ma pełną implementację. deButton zachowuje kontrolę przycisków. Gdy ma miejsce jakakolwiek akcja wprowadzana przez użytkownika, kod jest uruchamiany w trybie „zdarzenia sterującego”, w którym deButton wykrywa, że został naciśnięty i oznacza, że został naciśnięty, zwracając wartość TRUE. Zatem „if (deButton (...)) {... kod akcji ...} jest sposobem na dołączenie kodu akcji do przycisku, bez konieczności tworzenia zamknięcia lub pisania procedury obsługi zdarzenia. DD_THROW jest sposób kończenia przebiegu, gdy akcja jest wykonywana, ponieważ akcja mogła zmodyfikować dane aplikacji, więc kontynuowanie przebiegu „zdarzenia kontrolnego” przez procedurę jest nieprawidłowe. Jeśli porównasz to z pisaniem programów obsługi zdarzeń, oszczędza to ich zapisywania, i pozwala mieć dowolną liczbę kontrolek.
Dodano: Przepraszam, powinienem wyjaśnić, co rozumiem przez słowo „utrzymuje”. Kiedy procedura jest wykonywana po raz pierwszy (w trybie SHOW), deButton tworzy kontrolkę przycisku i zapamiętuje jej identyfikator w FIFO. Przy kolejnych przebiegach (w trybie UPDATE) deButton pobiera identyfikator z FIFO, modyfikuje go w razie potrzeby i umieszcza z powrotem w FIFO. W trybie ERASE czyta go z FIFO, niszczy i nie odkłada z powrotem, przez co „zbiera śmieci”. Dlatego wywołanie deButton zarządza całym okresem życia formantu, utrzymując go w zgodzie z danymi aplikacji, dlatego mówię, że „utrzymuje” go.
Czwarty tryb to ZDARZENIE (lub KONTROLA). Gdy użytkownik wpisze znak lub kliknie przycisk, to zdarzenie jest przechwytywane i rejestrowane, a następnie procedura deContents jest wykonywana w trybie EVENT. deButton pobiera identyfikator kontrolki przycisku z FIFO i pyta, czy to jest kliknięta kontrolka. Jeśli tak, zwraca TRUE, więc kod akcji może zostać wykonany. Jeśli nie, zwraca po prostu FALSE. Z drugiej strony deEdit(..., &myStringVar)
wykrywa, czy zdarzenie było przeznaczone dla niego, a jeśli tak, przekazuje je do kontrolki edycji, a następnie kopiuje zawartość kontrolki edycji do myStringVar. Między tym a normalnym przetwarzaniem UPDATE myStringVar zawsze równa się zawartości kontrolki edycji. W ten sposób odbywa się „wiązanie”. Ten sam pomysł dotyczy pasków przewijania, pól list, pól kombi, wszelkiego rodzaju elementów sterujących, które umożliwiają edycję danych aplikacji.
Oto link do mojej edycji Wikipedii: http://en.wikipedia.org/wiki/User:MikeDunlavey/Difex_Article