Sullo stile di codifica

 

Porto questo argomento perché ho una discreta esperienza di codifica e ricodifica di molto tempo fa scritta da zero in MQL4 e voglio condividere la mia esperienza.

Collega, non dubito della tua capacità di scrivere rapidamente un programma che implementa un algoritmo. Ma ho già fatto in modo che se hai abbandonato il progetto per qualsiasi motivo, ci sei tornato un mese dopo e non sei riuscito a capirlo immediatamente, non sei un buon scrittore. In ciò che segue vi racconterò le mie esigenze riguardo allo stile di codifica. L'osservanza di questi requisiti semplifica ulteriori modifiche.

0. Non precipitarsi subito nella battaglia e scrivere il programma. Fate come raccomandano i classici: passate qualche ora a pensare alla struttura del programma. Poi puoi sederti e scrivere un programma velocemente, in modo chiaro e conciso. Queste poche ore vi ripagheranno molte volte in velocità di scrittura e ulteriore debugging.

1. La lunghezza delle funzioni non dovrebbe superare significativamente le 20 righe. Se non potete implementarlo, ci sono alcuni punti in cui non avete pensato abbastanza bene alla logica e alla struttura del codice. Inoltre, è sulle funzioni più lunghe e sulla loro relazione con le funzioni che chiamano che si spende spesso la maggior parte del tempo nel debug del codice.

Per esempio, il mio codice ora è lungo 629 linee e contiene 27 funzioni. Questo insieme a una descrizione della struttura della chiamata di funzione (2-6 righe) e un breve commento prima di ogni funzione, così come 4-5 righe di separatori vuoti tra le funzioni. Inoltre, non metto le parentesi di blocco (parentesi graffe) con parsimonia, cioè per ognuna delle due parentesi dedico una riga. Se rimuovo tutti i commenti prima delle funzioni, e riduco il numero di separatori tra le funzioni, allora 27 funzioni prenderanno circa 400 linee, cioè la lunghezza media delle mie funzioni è di circa 15 linee.

Ci sono, naturalmente, delle eccezioni a questa regola - ma questo vale per le funzioni più semplici o per le funzioni di uscita. In generale, una funzione non dovrebbe eseguire più di 3-5 azioni funzionalmente diverse. Altrimenti, sarà confuso. Di solito metto anche una linea vuota tra le azioni funzionalmente diverse all'interno della funzione.

Ho un bel po' di funzioni che sono lunghe solo 4 righe (questo è un minimo, con una riga nel corpo della funzione, una per riga di dichiarazione e due per parentesi graffe) a 10 righe. Non sono preoccupato per la degradazione della velocità del codice a causa di questo, poiché il codice in realtà non rallenta affatto a causa di questo, ma a causa delle mani storte.

2. Non siate avari di commenti che spiegano i significati di azioni, variabili e funzioni. Il limite di 20 linee per la lunghezza della funzione rimane intatto in questo caso. Se lo rompi, riscrivi la funzione. Si possono usare sia commenti a linea singola che multilinea.

3. Le mie funzioni sono strutturate. Ci sono funzioni di livello di chiamata superiore (zero), 1°, 2°, ecc. Ogni funzione del prossimo livello di chiamata è chiamata direttamente dalla funzione del livello precedente. Per esempio, una funzione come questa:

// open
// .pairsToOpen
// .combineAndVerify( )
// Собирает из двух валют символ и выполняет все проверки, нужные для его открытия.
// Возвращает валидность пары для открытия.
// Последний аргумент - [...]
bool
combineAndVerify( string quoted, string base, double& fp1 )

- è una funzione di terzo livello. Qui:

open() è una funzione di primo livello,

pairsToOpen() è la seconda funzione (è chiamata da open()), e

combineAndVerify() - terzo (è chiamato dalla funzione pairsToOpen()).


Il codice di ogni funzione del livello successivo è indentato più a sinistra del codice della precedente. Questo rende più facile vedere la struttura dell'intero programma.

Ci sono anche eccezioni a questa regola (ci sono funzioni chiamate da due funzioni di un livello strutturale superiore), ma questo non è comune. Questo di solito indica che il codice non è ottimale, perché la stessa azione viene eseguita in diverse parti del programma.
Ci sono, tuttavia, funzioni universali che possono essere chiamate da qualsiasi luogo. Queste sono le funzioni di uscita, e le ho messe in una categoria speciale.

3. Variabili globali: è meglio averne meno, ma anche qui è meglio non esagerare. Potete infilare tutte queste variabili nei parametri formali delle funzioni, ma poi le loro chiamate saranno troppo ingombranti da scrivere e di significato oscuro.

4. Separare le azioni dei calcoli e il loro output (in un file, lo schermo o SMS). Disegno tutte le funzioni di uscita separatamente, e poi incollo le chiamate di tali funzioni nel corpo della funzione chiamante.

Questa regola, oltre a migliorare la chiarezza del codice, ha un altro effetto collaterale: se si fa così, si può molto facilmente tagliare tutto l'output dal codice e ridurre significativamente il tempo di esecuzione del codice: l'output è spesso l'azione più lenta in un programma.

5. Nomi di variabili: beh, questo è chiaro. Ognuno ha il suo stile, ma è comunque auspicabile farli in modo tale che spieghino facilmente il significato delle variabili.

Penso che sia sufficiente per iniziare. Se volete, potete aggiungerne altri.
 
Mathemat >> :
Penso che sia sufficiente per iniziare. Se volete, potete aggiungere qualcos'altro.

La domanda è questa. Qual è il modo più sensato di costruire un programma?

1. Descrivere tutto ciò che si può fare nella funzione START ?

2) O posso descrivere tutte le azioni come funzioni definite dall'utente, e poi chiamarle dalla funzione START come richiesto?

//---------------------------------------------

Per esempio, la stessa rete a strascico.

 

Il secondo è meglio. Questo è ciò di cui sto scrivendo. Anche le funzioni di trading dovrebbero essere scritte come funzioni separate.

 

Per me è quasi lo stesso.

Tranne:

1. Il numero di linee nella funzione.

2. Il numero di funzioni.

Io do la priorità alla velocità dei calcoli. Quindi, meno funzioni e meno ne chiamate, più velocemente il programma gira.

Se posso liberarmi di una funzione, la userò.

Solo una volta non sono riuscito a farlo. Le meta-citazioni imponevano un limite al numero di blocchi annidati.

Ho ottenuto una funzione di rendering dell'interfaccia di 710 linee. Ha 51 parametri. Ci sono 21 matrici. Così, questo è ciò che Metacquotes è riuscito ad ottenere... :-)))

In generale, penso che la funzione sia necessaria solo se viene chiamata da diverse parti del programma e non molto spesso. Preferisco ripetere il codice in ogni blocco per amore della velocità.

 
Zhunko >> :

Il risultato è una funzione di disegno di interfaccia di 710 linee. Ha 51 parametri. Ci sono 21 matrici.

Wow. Ma le funzioni di uscita, ho già notato, rappresentano un'eccezione. Per quanto riguarda la velocità di esecuzione, penso che il costo di chiamare una funzione invece di scrivere direttamente il blocco giusto senza una funzione non sia così grande - specialmente se la funzione è chiamata in un ciclo. Rosh ha mostrato da qualche parte la differenza tra codice diretto e codice di chiamata di funzione.

 
Zhunko писал(а) >>

Io do la priorità alla velocità di calcolo. Quindi, meno funzioni e meno ne chiamate, più velocemente il programma gira.

Se c'è un'opportunità di sbarazzarsi di una funzione, ne approfitto.

Sono d'accordo. Se una funzione viene chiamata meno di tre volte, è meglio inserirla nel corpo. Lo so per esperienza diretta. Spesso devo modificare i programmi di altre persone. Se tutto ciò che avete sono le funzioni, dovete aprire due o tre finestre per essere sicuri di non confondere ciò che succede quando.

 
Mathemat >> :

Wow. Ma le funzioni di uscita, ho già notato, rappresentano un'eccezione. Per quanto riguarda la velocità di esecuzione, penso che il costo di chiamare una funzione invece di scrivere direttamente il blocco richiesto senza una funzione non sia così grande - specialmente se la funzione è chiamata in un ciclo. Da qualche parte Rosh ha mostrato la differenza tra codice diretto e codice con chiamata di funzione.

Alexei, forse hai ragione, non ho controllato ultimamente, ma...!

Ci deve essere stato qualche problema in quei giorni con il gestore di memoria di MT4 per Metakvot. Quindi, avendo rimosso tutte le funzioni utilizzate per calcolare gli indici, sono rimasto molto sorpreso dal risultato!... La velocità di calcolo è aumentata di 5 volte e il consumo di memoria è diminuito di 3 volte!!!!

 
Zhunko писал(а) >>

Alexey, forse hai ragione, non ho controllato ultimamente, MA...!

Ci deve essere stato qualche problema in quei giorni con il gestore di memoria di Metacvot in MT4. Quindi, avendo rimosso tutte le funzioni utilizzate per calcolare gli indici, sono rimasto molto sorpreso dal risultato!... La velocità di calcolo è aumentata di 5 volte e il consumo di memoria è diminuito di 3 volte!!!!

Tutti gli array dichiarati nelle funzioni sono statici. Ciò significa che questi array sono creati solo una volta (durante la prima chiamata della funzione), e sono conservati in memoria. Pertanto, cerco di rendere gli array globali. Il che non è buono.

 
Sulla dimensione della funzione. Cerco di far stare la funzione su uno schermo. In modo che possiate vedere il tutto.
 

Sì, Vadim, l'impatto è lì. Ho deciso di controllare. Ecco i risultati:

1. Ciclo di somma semplice (500 milioni di iterazioni):


int start()
{
double sum = 0;
double d;
int st = GetTickCount();
for( int i = 0; i < 500000000; i ++ )
{
add( sum );


// sum += 3.14159265;

}
int timeTotal = GetTickCount() - st;
Print( "Time = " + timeTotal );
return(0);
}
//+------------------------------------------------------------------+


double add( double sum )
{
return( sum + 3.14159265 );
}//+------------------------------------------------------------------+


Tempo di calcolo in secondi: 4,42 - senza chiamare add(), 36,7 con esso.


2. Un ciclo con calcoli più complessi (gli stessi 500 milioni di iterazioni):


int start()
{
double sum = 0;
double d;
int st = GetTickCount();
for( int i = 0; i < 500000000; i ++ )
{
add( i, sum, d );


// d = MathTan( i ) + MathLog( i );
// sum += MathSin( 3.14159265 );
}
int timeTotal = GetTickCount() - st;
Print( "Time = " + timeTotal );
return(0);
}//+------------------------------------------------------------------+


double add( int i, double sum, double& d )
{
d = MathTan( i ) + MathLog( i );
return( sum + MathSin( 3.14159265 ) );
}//+------------------------------------------------------------------+


Tempo di calcolo in secondi: 100,6 senza add(), 142,1 con add().


Qui ci sono blocchi commentati con calcoli diretti nel ciclo che trasformiamo in una funzione per il confronto. Come possiamo vedere, c'è una differenza in ogni caso, ma è molto diversa.

Quali sono le conclusioni? Se formiamo qualcosa di molto semplice in una funzione, i costi di chiamata di funzione giocano un ruolo significativo, anche molto significativo. In altre parole, può essere molto più del costo dei calcoli nel corpo della funzione. Se i calcoli sono più complessi, la differenza tra la presenza della funzione e la sua assenza è molto ridotta.

Quindi è meglio progettare solo blocchi con calcoli più o meno seri in funzioni. Cercherò di tenerne conto durante la codifica. Ma in ogni caso la differenza di tempo è significativa solo quando il ciclo ha molte iterazioni: il costo della chiamata di funzione qui è circa 10^(-7) secondi.