C # 4.0: Dapatkah saya menggunakan TimeSpan sebagai parameter opsional dengan nilai default?

125

Keduanya menghasilkan kesalahan dengan mengatakan bahwa mereka harus berupa konstanta waktu kompilasi:

void Foo(TimeSpan span = TimeSpan.FromSeconds(2.0))
void Foo(TimeSpan span = new TimeSpan(2000))

Pertama-tama, bisakah seseorang menjelaskan mengapa nilai-nilai ini tidak dapat ditentukan pada waktu kompilasi? Dan apakah ada cara untuk menentukan nilai default untuk objek TimeSpan opsional?

Mike Pateras
sumber
11
Tidak terkait dengan apa yang Anda tanyakan, tetapi sadarilah bahwa new TimeSpan(2000)itu tidak berarti 2000 milidetik, itu berarti 2000 "kutu" yang merupakan 0,2 milidetik, atau satu per sepuluh dari dua detik.
Jeppe Stig Nielsen

Jawaban:

173

Anda dapat mengatasi ini dengan sangat mudah dengan mengubah tanda tangan Anda.

void Foo(TimeSpan? span = null) {

   if (span == null) { span = TimeSpan.FromSeconds(2); }

   ...

}

Saya harus menguraikan - alasan ungkapan-ungkapan dalam contoh Anda tidak konstanta waktu kompilasi adalah karena pada waktu kompilasi, kompiler tidak dapat hanya menjalankan TimeSpan.FromSeconds (2.0) dan menempelkan byte hasil ke dalam kode kompilasi Anda.

Sebagai contoh, pertimbangkan jika Anda mencoba menggunakan DateTime. Sekarang sebagai gantinya. Nilai DateTime. Sekarang berubah setiap kali dieksekusi. Atau anggap TimeSpan.FromSeconds mempertimbangkan gravitasi. Ini adalah contoh yang tidak masuk akal tetapi aturan konstanta waktu kompilasi tidak membuat kasus khusus hanya karena kita mengetahui bahwa TimeSpan.

Josh
sumber
15
Sekarang dokumentasikan nilai default di <param>, karena tidak terlihat di tanda tangan.
Kolonel Panic
3
Saya tidak bisa melakukan ini, saya menggunakan nilai khusus null untuk sesuatu yang lain.
Kolonel Panic
4
@MattHickford - Maka Anda harus memberikan metode kelebihan beban atau mengambil milidetik sebagai parameter.
Josh
19
Dapat juga digunakan span = span ?? TimeSpan.FromSeconds(2.0);dengan tipe nullable, di method body. Atau var realSpan = span ?? TimeSpan.FromSeconds(2.0);untuk mendapatkan variabel lokal yang tidak dapat dibatalkan.
Jeppe Stig Nielsen
5
Hal yang saya tidak suka tentang ini adalah bahwa ini menyiratkan kepada pengguna fungsi bahwa fungsi ini "berfungsi" dengan rentang nol. Tapi itu tidak benar! Null bukan nilai yang valid untuk rentang sejauh logika aktual dari fungsi yang bersangkutan. Saya berharap ada cara yang lebih baik yang tidak tampak seperti bau kode ...
JoeCool
31

Warisan VB6 saya membuat saya tidak nyaman dengan gagasan untuk mempertimbangkan "nilai nol" dan "nilai hilang" menjadi setara. Dalam kebanyakan kasus, ini mungkin baik-baik saja, tetapi Anda mungkin memiliki efek samping yang tidak diinginkan, atau Anda mungkin menelan kondisi yang luar biasa (misalnya, jika sumbernya spanadalah properti atau variabel yang tidak boleh nol, tetapi adalah).

Karena itu saya akan membebani metode:

void Foo()
{
    Foo(TimeSpan.FromSeconds(2.0));
}
void Foo(TimeSpan span)
{
    //...
}
phoog
sumber
1
+1 untuk teknik hebat itu. Parameter default seharusnya hanya digunakan dengan tipe const, sungguh. Lain, itu tidak bisa diandalkan.
Lazlo
2
Ini adalah pendekatan 'waktu terhormat' yang menggantikan nilai default, dan untuk situasi ini saya pikir ini adalah jawaban yang paling jelek;) Dengan sendirinya itu tidak selalu berfungsi dengan baik untuk antarmuka, karena Anda benar-benar menginginkan nilai default di satu tempat. Dalam hal ini saya telah menemukan metode ekstensi menjadi alat yang berguna: antarmuka memiliki satu metode dengan semua parameter, kemudian serangkaian metode ekstensi dinyatakan dalam kelas statis di samping antarmuka mengimplementasikan default di berbagai kelebihan beban.
OlduwanSteve
23

Ini berfungsi dengan baik:

void Foo(TimeSpan span = default(TimeSpan))

Elena Lavrinenko
sumber
4
Selamat datang di Stack Overflow. Jawaban Anda tampaknya adalah Anda dapat memberikan nilai parameter default, asalkan itu nilai yang sangat spesifik yang diizinkan oleh kompiler. Sudahkah saya mengerti itu? (Anda dapat mengedit jawaban Anda untuk mengklarifikasi.) Ini akan menjadi jawaban yang lebih baik jika itu menunjukkan bagaimana memanfaatkan apa yang memungkinkan kompiler untuk mendapatkan apa yang awalnya dicari pertanyaan, yang memiliki nilai - TimeSpan nilai lain yang sewenang - wenang , seperti yang diberikan oleh new TimeSpan(2000).
Rob Kennedy
2
Alternatif yang menggunakan beberapa nilai default spesifik adalah dengan menggunakan static private readonly TimeSpan defaultTimespan = Timespan.FromSeconds (2) dikombinasikan dengan konstruktor default dan konstruktor mengambil rentang waktu. public Foo (): this (defaultTimespan) dan Foo publik (Timespan ts)
johan mårtensson
15

Himpunan nilai yang dapat digunakan sebagai nilai default sama dengan yang dapat digunakan untuk argumen atribut. Alasannya adalah bahwa nilai default dikodekan ke dalam metadata di dalam DefaultParameterValueAttribute.

Mengapa tidak dapat ditentukan pada waktu kompilasi. Rangkaian nilai dan ekspresi atas nilai-nilai tersebut yang diizinkan pada waktu kompilasi tercantum dalam spesifikasi bahasa C # resmi :

C # 6.0 - Jenis parameter atribut :

Jenis parameter posisi dan nama untuk kelas atribut terbatas pada jenis parameter atribut , yaitu:

  • Salah satu jenis berikut: bool, byte, char, double, float, int, long, sbyte, short, string, uint, ulong, ushort.
  • Jenisnya object.
  • Jenisnya System.Type.
  • Tipe enum.
    (asalkan memiliki aksesibilitas publik dan jenis tempat bersarangnya (jika ada) juga memiliki aksesibilitas publik)
  • Array satu dimensi dari tipe di atas.

Tipe TimeSpanini tidak cocok dengan salah satu dari daftar ini dan karenanya tidak dapat digunakan sebagai konstanta.

JaredPar
sumber
2
Nitight pick sedikit: Memanggil metode statis tidak cocok dengan daftar mana pun. TimeSpanbisa muat yang terakhir dalam daftar default(TimeSpan)ini valid.
CodesInChaos
12
void Foo(TimeSpan span = default(TimeSpan))
{
    if (span == default(TimeSpan)) 
        span = TimeSpan.FromSeconds(2); 
}

asalkan default(TimeSpan)bukan nilai yang valid untuk fungsi tersebut.

Atau

//this works only for value types which TimeSpan is
void Foo(TimeSpan span = new TimeSpan())
{
    if (span == new TimeSpan()) 
        span = TimeSpan.FromSeconds(2); 
}

asalkan new TimeSpan()bukan nilai yang valid.

Atau

void Foo(TimeSpan? span = null)
{
    if (span == null) 
        span = TimeSpan.FromSeconds(2); 
}

Ini harus lebih baik mengingat peluang nullnilai sebagai nilai yang valid untuk fungsi jarang terjadi.

nawfal
sumber
4

TimeSpanadalah kasus khusus untuk DefaultValueAttributedan ditentukan menggunakan string apa pun yang dapat diuraikan melalui TimeSpan.Parsemetode ini.

[DefaultValue("0:10:0")]
public TimeSpan Duration { get; set; }
dahall
sumber
3

Saran saya:

void A( long spanInMs = 2000 )
{
    var ts = TimeSpan.FromMilliseconds(spanInMs);

    //...
}

BTW TimeSpan.FromSeconds(2.0)tidak sama new TimeSpan(2000)- konstruktor mengambil kutu.

tymtam
sumber
2

Jawaban lain telah memberikan penjelasan yang bagus mengapa parameter opsional tidak bisa menjadi ekspresi dinamis. Tetapi, untuk menghitung ulang, parameter default berperilaku seperti konstanta waktu kompilasi. Itu artinya kompiler harus dapat mengevaluasinya dan memberikan jawaban. Ada beberapa orang yang ingin C # menambahkan dukungan untuk kompiler yang mengevaluasi ekspresi dinamis ketika menghadapi deklarasi konstan — fitur semacam ini akan terkait dengan metode menandai "murni", tetapi itu bukan kenyataan saat ini dan mungkin tidak akan pernah terjadi.

Salah satu alternatif untuk menggunakan parameter default C # untuk metode seperti itu adalah dengan menggunakan pola yang dicontohkan oleh XmlReaderSettings. Dalam pola ini, tentukan kelas dengan konstruktor tanpa parameter dan properti yang dapat ditulis untuk umum. Kemudian ganti semua opsi dengan default di metode Anda dengan objek jenis ini. Bahkan membuat objek ini opsional dengan menentukan default nulluntuk itu. Sebagai contoh:

public class FooSettings
{
    public TimeSpan Span { get; set; } = TimeSpan.FromSeconds(2);

    // I imagine that if you had a heavyweight default
    // thing you’d want to avoid instantiating it right away
    // because the caller might override that parameter. So, be
    // lazy! (Or just directly store a factory lambda with Func<IThing>).
    Lazy<IThing> thing = new Lazy<IThing>(() => new FatThing());
    public IThing Thing
    {
        get { return thing.Value; }
        set { thing = new Lazy<IThing>(() => value); }
    }

    // Another cool thing about this pattern is that you can
    // add additional optional parameters in the future without
    // even breaking ABI.
    //bool FutureThing { get; set; } = true;

    // You can even run very complicated code to populate properties
    // if you cannot use a property initialization expression.
    //public FooSettings() { }
}

public class Bar
{
    public void Foo(FooSettings settings = null)
    {
        // Allow the caller to use *all* the defaults easily.
        settings = settings ?? new FooSettings();

        Console.WriteLine(settings.Span);
    }
}

Untuk menelepon, gunakan satu sintaks aneh itu untuk membuat instance dan menetapkan properti semua dalam satu ekspresi:

bar.Foo(); // 00:00:02
bar.Foo(new FooSettings { Span = TimeSpan.FromDays(1), }); // 1.00:00:00
bar.Foo(new FooSettings { Thing = new MyCustomThing(), }); // 00:00:02

Kerugian

Ini adalah pendekatan yang sangat berat untuk menyelesaikan masalah ini. Jika Anda menulis antarmuka internal yang cepat dan kotor dan membuat TimeSpannullable dan memperlakukan null seperti nilai default yang Anda inginkan akan berfungsi dengan baik, lakukan itu.

Juga, jika Anda memiliki sejumlah besar parameter atau memanggil metode dalam loop ketat, ini akan memiliki overhead instantiations kelas. Tentu saja, jika memanggil metode seperti itu dalam loop ketat, itu mungkin alami dan bahkan sangat mudah untuk menggunakan kembali instance FooSettingsobjek.

Manfaat

Seperti yang saya sebutkan dalam komentar di contoh, saya pikir pola ini bagus untuk API publik. Menambahkan properti baru ke kelas adalah perubahan ABI yang tidak terputus-putus, sehingga Anda dapat menambahkan parameter opsional baru tanpa mengubah tanda tangan metode Anda menggunakan pola ini — memberikan lebih banyak kode yang dikompilasi baru-baru ini lebih banyak pilihan sambil terus mendukung kode yang dikompilasi lama tanpa kerja ekstra .

Selain itu, karena parameter metode bawaan bawaan C # diperlakukan sebagai konstanta waktu-kompas dan dimasukkan ke dalam lokasi panggilan, parameter default hanya akan digunakan oleh kode setelah dikompilasi ulang. Dengan membuat instance objek pengaturan, pemanggil secara dinamis memuat nilai default saat memanggil metode Anda. Ini berarti bahwa Anda dapat memperbarui default dengan hanya mengubah kelas pengaturan Anda. Dengan demikian, pola ini memungkinkan Anda mengubah nilai default tanpa harus mengkompilasi ulang penelepon untuk melihat nilai-nilai baru, jika itu diinginkan.

binki
sumber