Perché Math.Sqrt () è una funzione statica?

30

In una discussione sui metodi statici e di istanza, penso sempre, che Sqrt() dovrebbe essere un metodo di istanza di tipi numerici invece di un metodo statico. Perché? Ovviamente funziona su un valore.

 // looks wrong to me
 var y = Math.Sqrt(x);
 // looks better to me
 var y = x.Sqrt();

Ovviamente i tipi di valore possono avere metodi di istanza, come in molte lingue, esiste un metodo di istanza ToString() .

Per rispondere ad alcune domande dai commenti: perché 1.Sqrt() non dovrebbe essere legale? 1.ToString() è.

Alcune lingue non consentono di avere metodi sui tipi di valore, ma alcune lingue possono. Sto parlando di questi, tra cui Java, ECMAScript, C # e Python (con __str__(self) definito). Lo stesso vale per altre funzioni come ceil() , floor() ecc.

    
posta Residuum 03.11.2015 - 15:04
fonte

10 risposte

19

È interamente una scelta di design della lingua. Dipende anche dall'implementazione sottostante dei tipi primitivi e dalle considerazioni sulle prestazioni a causa di ciò.

.NET ha solo uno statico Math.Sqrt metodo che agisce su double e restituisce un double . Tutto il resto che passi a questo deve essere lanciato o promosso a double .

double sqrt2 = Math.Sqrt(2d);

D'altra parte, hai Rust che espone queste operazioni come funzioni sui tipi :

let sqrt2 = 2.0f32.sqrt();
let higher = 2.0f32.max(3.0f32);

Ma Rust ha anche una sintassi di chiamata della funzione universale (qualcuno ha menzionato prima), quindi puoi scegliere quello che preferisci.

let sqrt2 = f32::sqrt(2.0f32);
let higher = f32::max(2.0f32, 3.0f32);
    
risposta data 04.11.2015 - 14:58
fonte
65

Supponiamo di progettare una nuova lingua e vogliamo che Sqrt sia un metodo di istanza. Quindi osserviamo la classe double e iniziamo a progettare. Ovviamente non ha input (tranne l'istanza) e restituisce un double . Scriviamo e testiamo il codice. La perfezione.

Ma anche la radice quadrata di un intero è valida e non vogliamo forzare tutti a convertire in un doppio solo per prendere una radice quadrata. Quindi passiamo a int e iniziamo la progettazione. Cosa restituisce? Noi potremmo restituire un int e farlo funzionare solo per i quadrati perfetti, o arrotondare il risultato al più vicino int (ignorando il dibattito sul metodo di arrotondamento corretto per ora). Ma cosa succede se qualcuno vuole un risultato non intero? Dovremmo avere due metodi: uno che restituisce un int e uno che restituisce un double (che non è possibile in alcune lingue senza cambiare il nome). Quindi decidiamo che dovrebbe restituire un double . Ora implementiamo. Ma l'implementazione è identica a quella che abbiamo usato per double . Facciamo copia e incolla? Lanciamo l'istanza a double e chiamiamo quel metodo di istanza? Perché non inserire la logica in un metodo di libreria a cui è possibile accedere da entrambe le classi. Chiameremo la libreria Math e la funzione Math.Sqrt .

Why is Math.Sqrt a static function?:

  • Perché l'implementazione è la stessa indipendentemente dal tipo numerico sottostante
  • Perché non influisce su una particolare istanza (accetta un valore e restituisce un risultato)
  • Poiché i tipi numerici non dipendono su quella funzionalità, quindi ha senso averlo in una classe separata

Non abbiamo nemmeno affrontato altri argomenti:

  • Dovrebbe essere chiamato GetSqrt in quanto restituisce un nuovo valore anziché modificare l'istanza?
  • Che dire di Square ? %codice%? %codice%? %codice%? %codice%? %codice%? %codice%? %codice%? %codice%? %codice%?
risposta data 03.11.2015 - 15:41
fonte
26

Le operazioni matematiche sono spesso molto sensibili alle prestazioni. Pertanto, vorremmo utilizzare metodi statici che possono essere completamente risolti (e ottimizzati o inline) in fase di compilazione. Alcune lingue non offrono alcun meccanismo per specificare metodi inviati staticamente. Inoltre, il modello a oggetti di molte lingue ha un sovraccarico di memoria considerevole inaccettabile per tipi "primitivi" come double .

Alcune lingue ci consentono di definire funzioni che utilizzano la sintassi di richiamo del metodo, ma in realtà vengono inviate staticamente. I metodi di estensione in C # 3.0 o successivi sono un esempio. I metodi non virtuali (ad esempio l'impostazione predefinita per i metodi in C ++) sono un altro caso, sebbene il C ++ non supporti i metodi sui tipi primitivi. Ovviamente potresti creare la tua classe wrapper in C ++ che decora un tipo primitivo con vari metodi, senza sovraccarico di runtime. Tuttavia, dovrai convertire manualmente i valori in quel tipo di wrapper.

Ci sono un paio di lingue che definiscono metodi sui loro tipi numerici. Di solito sono lingue altamente dinamiche in cui tutto è un oggetto. Qui, le prestazioni sono una considerazione secondaria dell'eleganza concettuale, ma quelle lingue non sono generalmente utilizzate per il calcolo del numero. Tuttavia, queste lingue potrebbero avere un ottimizzatore che può "unbox" operazioni su primitive.

Considerando le considerazioni tecniche, possiamo considerare se un'interfaccia matematica basata su metodo possa essere una buona interfaccia. Sorgono due problemi:

  • la notazione matematica è basata su operatori e funzioni, non su metodi. Un'espressione come 42.sqrt apparirà molto più estranea a molti utenti di sqrt(42) . Come utente matematico, preferirei piuttosto la possibilità di creare i miei operatori sulla sintassi delle chiamate punti-metodo.
  • il principio di responsabilità unica ci incoraggia a limitare il numero di operazioni che fanno parte di un tipo alle operazioni essenziali. Rispetto alla moltiplicazione, la radice quadrata richiede una notevole rarità. Se la tua lingua è specificatamente intesa per l'analisi statistica, puoi fornire più primitive (come le operazioni mean , median , variance , std , normalize su elenchi numerici o la funzione Gamma per i numeri) utile. Per un linguaggio generico, questo pesa solo l'interfaccia. Il relegare le operazioni non essenziali in uno spazio dei nomi separato rende il tipo più accessibile per la maggior parte degli utenti.
risposta data 03.11.2015 - 15:55
fonte
15

Sarei motivato dal fatto che ci sono un sacco di funzioni matematiche per scopi speciali, e piuttosto che popolare ogni tipo di matematica con tutti (o un sottoinsieme casuale) di quelle funzioni che le hai messe in una classe di utilità. Altrimenti, avresti inquinato il suggerimento di completamento automatico o forzerai le persone a cercare sempre in due punti. ( sin è abbastanza importante da essere un membro di Double , oppure è nella classe Math insieme a inbreds come htan e exp1p ?)

Un altro motivo pratico è che risulta che ci possono essere diversi modi per implementare metodi numerici, con prestazioni diverse e compromessi di precisione. Java ha Math , e ha anche StrictMath .

    
risposta data 03.11.2015 - 19:32
fonte
6

Hai correttamente osservato che c'è una curiosa simmetria in gioco qui.

Se dico sqrt(n) o n.sqrt() non importa, entrambi esprimono la stessa cosa e quello che preferisci è più una questione di gusti personali che altro.

Questo è anche il motivo per cui esiste una strong argomentazione di alcuni progettisti di linguaggi per rendere le due sintassi intercambiabili. Il linguaggio di programmazione D consente già questo sotto una funzione chiamata Sintassi di chiamata delle funzioni uniformi . Una caratteristica simile è stata anche proposta per la standardizzazione in C ++ . Come Mark Amery sottolinea nei commenti , Python consente anche questo.

Questo non è senza problemi. L'introduzione di un cambiamento fondamentale della sintassi come questa ha conseguenze di ampia portata per il codice esistente ed è ovviamente anche un argomento di discussioni controverse tra sviluppatori che sono stati addestrati per decenni a pensare alle due sintassi come a descrivere cose diverse.

Credo che solo il tempo dirà se l'unificazione dei due è fattibile a lungo termine, ma è sicuramente una considerazione interessante.

    
risposta data 04.11.2015 - 13:57
fonte
3

Oltre alla risposta di D Stanley devi pensare al polimorfismo. Metodi come Math.Sqrt dovrebbero sempre restituire lo stesso valore allo stesso input. Rendere il metodo statico è un buon modo per chiarire questo punto, poiché i metodi statici non sono sovrascrivibili.

Hai menzionato il metodo ToString () -. Qui puoi decidere di sovrascrivere questo metodo, quindi la (sotto) classe è rappresentata in un altro modo come String della sua classe genitore. Quindi rendi un metodo di istanza.

    
risposta data 03.11.2015 - 22:00
fonte
2

Bene, in Java c'è un wrapper per ogni tipo di base.
E i tipi base non sono tipi di classi e non hanno funzioni membro.

Quindi hai le seguenti scelte:

  1. Raccogli tutte le funzioni helper in una classe pro forma come Math .
  2. Imposta una funzione statica sul wrapper corrispondente.
  3. Rendilo una funzione membro sul wrapper corrispondente.
  4. Modifica le regole di Java.

Escludiamo l'opzione 4, perché ... Java è Java, e gli aderenti dichiarano che piace così.

Ora, possiamo anche escludere l'opzione 3 perché l'allocazione degli oggetti è piuttosto economica, non è gratuita, e facendo ciò ripetutamente fa .

Due giù, uno ancora da uccidere: l'opzione 2 è anche una cattiva idea, perché significa che ogni deve essere implementata per il tipo ogni , non si può fare affidamento sull'ampliamento conversione per colmare le lacune, o le incoerenze faranno davvero male.
E dando un'occhiata a java.lang.Math , ci sono lotti di spazi vuoti, soprattutto per tipi più piccoli di int rispettivi double .

Quindi, alla fine il chiaro vincitore è l'opzione uno, raccogliendoli tutti in un unico posto in una classe di funzione dell'utilità.

Ritornando all'opzione 4, qualcosa in quella direzione è accaduto molto più tardi: puoi chiedere al compilatore di considerare tutti i membri statici di qualsiasi classe che desideri quando risolvono i nomi da un po 'di tempo. import static someclass.*;

Per inciso, altri linguaggi non hanno questo problema, sia perché non hanno alcun pregiudizio contro le funzioni libere (facoltativamente usando i namespace) o molto meno piccoli tipi.

    
risposta data 03.11.2015 - 23:47
fonte
2

Un punto che non vedo menzionato esplicitamente (anche se amon lo allude) è che la radice quadrata può essere pensata come un'operazione "derivata": se l'implementazione non la fornisce per noi, possiamo scrivere il nostro proprio.

Poiché la domanda è codificata con il design del linguaggio, potremmo prendere in considerazione alcune descrizioni indipendenti dal linguaggio. Sebbene molte lingue abbiano filosofie diverse, è molto comune attraverso i paradigmi utilizzare l'incapsulamento per preservare gli invarianti; vale a dire evitare di avere un valore che non si comporta come suggerirebbe il suo tipo.

Ad esempio, se abbiamo qualche implementazione di interi usando le parole macchina, probabilmente vorremmo incapsulare la rappresentazione in qualche modo (ad esempio per impedire cambiamenti di bit dalla modifica del segno), ma allo stesso tempo abbiamo ancora bisogno di accedere a quei bit per implementare operazioni come addizione.

Alcune lingue potrebbero implementarlo con classi e metodi privati:

class Int {
    public Int add(Int x) {
      // Do something with the bits
    }
    private List<Boolean> getBits() {
      // ...
    }
}

Alcuni con i sistemi dei moduli:

signature INT = sig
  type int
  val add : int -> int -> int
end

structure Word : INT = struct
  datatype int  = (* ... *)
  fun add x y   = (* Do something with the bits *)
  fun getBits x = (* ... *)
end

Alcuni con ambito lessicale:

(defun getAdder ()
   (let ((getBits (lambda (x) ; ...
         (add     (lambda (x y) ; Do something with the bits
     'add))

E così via. Tuttavia, nessuno di questi meccanismi è necessario per l'implementazione della radice quadrata: può essere implementato usando l'interfaccia pubblica di un tipo numerico, e quindi non ha bisogno di accedere al dettagli di implementazione incapsulati.

Quindi la posizione della radice quadrata dipende dalla filosofia / dal gusto della lingua e dal progettista della biblioteca. Alcuni possono scegliere di mettere "dentro" i valori numerici (ad esempio, renderlo un metodo di istanza), alcuni possono scegliere di metterlo allo stesso livello delle operazioni primitive (questo potrebbe significare un metodo di istanza, o potrebbe significare vivere all'esterno i valori numerici, ma dentro lo stesso modulo / classe / spazio dei nomi, ad esempio come funzione autonoma o metodo statico), alcuni potrebbero scegliere di inserirlo in una raccolta di "helper" funzioni, alcuni potrebbero scegliere di delegarlo a librerie di terze parti.

    
risposta data 04.11.2015 - 13:23
fonte
-2

In Java e C # ToString è un metodo di oggetto, la radice della gerarchia di classi, quindi ogni oggetto implementerà il metodo ToString. Per un Integertype è naturale che l'implementazione di ToString funzioni in questo modo.

Quindi il tuo ragionamento è sbagliato. I tipi di valore della ragione implementano ToString non è che alcune persone fossero come: hey, abbiamo un metodo ToString per i tipi di valore. È perché ToString è già lì ed è "la cosa più naturale" da produrre.

    
risposta data 04.11.2015 - 14:08
fonte
-3

Diversamente da String.substring, Number.sqrt non è realmente un attributo del numero ma piuttosto un nuovo risultato basato sul tuo numero. Penso che passare il tuo numero a una funzione di squadratura sia più intuitivo.

Inoltre, l'oggetto Math contiene altri membri statici e ha più senso raggrupparli insieme e usarli in modo uniforme.

    
risposta data 03.11.2015 - 22:11
fonte