C # 4.0: ฉันสามารถใช้ TimeSpan เป็นพารามิเตอร์เสริมที่มีค่าเริ่มต้นได้หรือไม่


125

ทั้งสองอย่างนี้สร้างข้อผิดพลาดที่บอกว่าต้องเป็นค่าคงที่เวลาคอมไพล์:

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

ก่อนอื่นใครสามารถอธิบายได้ว่าทำไมค่าเหล่านี้ไม่สามารถกำหนดได้ในเวลารวบรวม? และมีวิธีระบุค่าดีฟอลต์สำหรับอ็อบเจ็กต์ TimeSpan หรือไม่?


11
ไม่เกี่ยวข้องกับสิ่งที่คุณถาม แต่โปรดทราบว่านั่นnew TimeSpan(2000)ไม่ได้หมายถึง 2,000 มิลลิวินาทีมันหมายถึง "เห็บ" 2,000 ตัวซึ่งเท่ากับ 0.2 มิลลิวินาทีหรือหนึ่ง 10,000 วินาทีของสองวินาที
Jeppe Stig Nielsen

คำตอบ:


173

คุณสามารถแก้ไขปัญหานี้ได้อย่างง่ายดายโดยเปลี่ยนลายเซ็นของคุณ

void Foo(TimeSpan? span = null) {

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

   ...

}

ฉันควรอธิบายให้ละเอียด - เหตุผลที่นิพจน์เหล่านั้นในตัวอย่างของคุณไม่ใช่ค่าคงที่เวลาคอมไพล์เนื่องจากในเวลาคอมไพล์คอมไพเลอร์ไม่สามารถเรียกใช้งาน TimeSpan.FromSeconds (2.0) และใส่ไบต์ของผลลัพธ์ลงในโค้ดที่คอมไพล์ของคุณได้

ตัวอย่างเช่นพิจารณาว่าคุณพยายามใช้ DateTime.Now แทนหรือไม่ ค่าของ DateTime ตอนนี้เปลี่ยนทุกครั้งที่ดำเนินการ หรือสมมติว่า TimeSpan.FromSeconds คำนึงถึงแรงโน้มถ่วง เป็นตัวอย่างที่ไร้สาระ แต่กฎของค่าคงที่เวลาคอมไพล์ไม่ได้สร้างกรณีพิเศษเพียงเพราะเรารู้ว่า TimeSpan.FromSeconds เป็นตัวกำหนด


15
ตอนนี้บันทึกค่าเริ่มต้น<param>เนื่องจากไม่ปรากฏในลายเซ็น
พันเอก Panic

3
ฉันทำสิ่งนี้ไม่ได้ฉันกำลังใช้ค่าว่างพิเศษสำหรับอย่างอื่น
พันเอก Panic

4
@MattHickford - จากนั้นคุณจะต้องระบุวิธีการที่โอเวอร์โหลดหรือใช้เวลาเป็นมิลลิวินาทีเป็นพารามิเตอร์
Josh

19
ยังสามารถใช้span = span ?? TimeSpan.FromSeconds(2.0);กับประเภท nullable ใน method body หรือvar realSpan = span ?? TimeSpan.FromSeconds(2.0);เพื่อรับตัวแปรโลคัลซึ่งไม่เป็นโมฆะ
Jeppe Stig Nielsen

5
สิ่งที่ฉันไม่ชอบเกี่ยวกับเรื่องนี้ก็คือมันบอกเป็นนัยว่าให้ผู้ใช้ฟังก์ชันนั้นฟังก์ชั่นนี้ "ทำงาน" ด้วยช่วงว่าง แต่นั่นไม่ใช่เรื่องจริง! Null ไม่ใช่ค่าที่ถูกต้องสำหรับสแปนเท่าที่เกี่ยวข้องกับตรรกะที่แท้จริงของฟังก์ชัน ฉันหวังว่าจะมีวิธีที่ดีกว่าที่ดูเหมือนไม่มีกลิ่นรหัส ...
JoeCool

31

มรดก VB6 ของฉันทำให้ฉันไม่สบายใจกับความคิดที่จะพิจารณา "ค่าว่าง" และ "ค่าที่ขาดหายไป" ให้เทียบเท่ากัน ในกรณีส่วนใหญ่อาจเป็นเรื่องปกติ แต่คุณอาจมีผลข้างเคียงที่ไม่ได้ตั้งใจหรือคุณอาจกลืนเงื่อนไขพิเศษ (เช่นหากแหล่งที่มาspanเป็นคุณสมบัติหรือตัวแปรที่ไม่ควรเป็นค่าว่าง แต่เป็น)

ดังนั้นฉันจะใช้วิธีนี้มากเกินไป:

void Foo()
{
    Foo(TimeSpan.FromSeconds(2.0));
}
void Foo(TimeSpan span)
{
    //...
}

1
+1 สำหรับเทคนิคที่ยอดเยี่ยมนั้น ควรใช้พารามิเตอร์เริ่มต้นกับประเภท const เท่านั้นจริงๆ อย่างอื่นมันไม่น่าเชื่อถือ
Lazlo

2
นี่เป็นวิธี 'เวลาที่ได้รับเกียรติ' ที่แทนที่ค่าเริ่มต้นและสำหรับสถานการณ์นี้ฉันคิดว่านี่เป็นคำตอบที่น่าเกลียดน้อยที่สุด) ในตัวของมันเองมันไม่จำเป็นต้องทำงานได้ดีสำหรับอินเทอร์เฟซเพราะคุณต้องการค่าเริ่มต้นใน ที่เดียว. ในกรณีนี้ฉันพบว่าวิธีการส่วนขยายเป็นเครื่องมือที่มีประโยชน์: อินเทอร์เฟซมีวิธีการเดียวที่มีพารามิเตอร์ทั้งหมดจากนั้นชุดของวิธีการขยายที่ประกาศในคลาสแบบคงที่ควบคู่ไปกับอินเทอร์เฟซจะใช้ค่าเริ่มต้นในการโอเวอร์โหลดต่างๆ
OlduwanSteve

23

ใช้งานได้ดี:

void Foo(TimeSpan span = default(TimeSpan))


4
ยินดีต้อนรับสู่ Stack Overflow คำตอบของคุณดูเหมือนว่าคุณสามารถระบุค่าพารามิเตอร์เริ่มต้นได้ตราบใดที่เป็นค่าเฉพาะที่คอมไพลเลอร์อนุญาต ฉันเข้าใจแล้วใช่ไหม (คุณสามารถแก้ไขคำตอบของคุณเพื่อชี้แจงได้) นี่จะเป็นคำตอบที่ดีกว่าหากมันแสดงให้เห็นว่าจะใช้ประโยชน์จากสิ่งที่คอมไพเลอร์อนุญาตให้ได้รับสิ่งที่คำถามเดิมต้องการได้อย่างไรซึ่งจะมีค่าอื่น ๆ ตามอำเภอใจTimeSpanเช่นที่กำหนดโดยnew TimeSpan(2000).
Rob Kennedy

2
อีกทางเลือกหนึ่งที่ใช้ค่าดีฟอลต์ที่เฉพาะเจาะจงคือการใช้ TimeSpan แบบคงที่ส่วนตัวแบบอ่านอย่างเดียว defaultTimespan = Timespan.FromSeconds (2) รวมกับตัวสร้างเริ่มต้นและตัวสร้างที่ใช้ช่วงเวลา Foo สาธารณะ (): สิ่งนี้ (defaultTimespan) และ Foo สาธารณะ (Timespan ts)
johan mårtensson

15

ชุดของค่าที่สามารถใช้เป็นค่าเริ่มต้นจะเหมือนกับที่สามารถใช้สำหรับอาร์กิวเมนต์แอตทริบิวต์ สาเหตุที่ค่าเริ่มต้นถูกเข้ารหัสเป็นข้อมูลเมตาภายในไฟล์DefaultParameterValueAttribute.

ถึงสาเหตุที่ไม่สามารถระบุเวลาคอมไพล์ได้ ชุดของค่าและนิพจน์เหนือค่าดังกล่าวที่อนุญาตในเวลาคอมไพล์แสดงอยู่ในข้อกำหนดภาษา C #อย่างเป็นทางการ:

C # 6.0 - ประเภทพารามิเตอร์แอตทริบิวต์ :

ประเภทของพารามิเตอร์ตำแหน่งและชื่อสำหรับคลาสแอ็ตทริบิวต์ถูก จำกัด ไว้ที่ประเภทพารามิเตอร์แอ็ตทริบิวต์ซึ่ง ได้แก่ :

  • ประเภทใดประเภทหนึ่งต่อไปนี้: bool` byte` char, double` float` int, long` sbyte` short, string` uint` ulong, ushort.
  • objectประเภท
  • System.Typeประเภท
  • ประเภท enum
    (หากมีการเข้าถึงแบบสาธารณะและประเภทที่ซ้อนอยู่ (ถ้ามี) มีการเข้าถึงแบบสาธารณะด้วย)
  • อาร์เรย์มิติเดียวประเภทข้างต้น

ประเภทTimeSpanไม่พอดีกับรายการเหล่านี้ดังนั้นจึงไม่สามารถใช้เป็นค่าคงที่ได้


2
เล็กน้อย nit-pick: การเรียกวิธีการแบบคงที่ไม่พอดีกับรายการใด ๆ TimeSpanสามารถใส่อันสุดท้ายในรายการdefault(TimeSpan)นี้ได้ถูกต้อง
CodesInChaos

12
void Foo(TimeSpan span = default(TimeSpan))
{
    if (span == default(TimeSpan)) 
        span = TimeSpan.FromSeconds(2); 
}

ที่ระบุdefault(TimeSpan)ไม่ใช่ค่าที่ถูกต้องสำหรับฟังก์ชัน

หรือ

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

ที่ระบุnew TimeSpan()ไม่ใช่ค่าที่ถูกต้อง

หรือ

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

สิ่งนี้ควรจะดีกว่าเมื่อพิจารณาถึงโอกาสที่nullค่าจะเป็นค่าที่ถูกต้องสำหรับฟังก์ชันนั้นหายาก


4

TimeSpanเป็นกรณีพิเศษสำหรับDefaultValueAttributeและระบุโดยใช้สตริงใด ๆ ที่สามารถแยกวิเคราะห์ผ่านTimeSpan.Parseวิธีการ

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

3

คำแนะนำของฉัน:

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

    //...
}

BTW TimeSpan.FromSeconds(2.0)ไม่เท่ากันnew TimeSpan(2000)- ตัวสร้างใช้เห็บ


2

คำตอบอื่น ๆได้ให้คำอธิบายที่ดีว่าเหตุใดพารามิเตอร์ทางเลือกจึงไม่สามารถเป็นนิพจน์แบบไดนามิกได้ แต่หากต้องการนับใหม่พารามิเตอร์เริ่มต้นจะทำงานเหมือนค่าคงที่ของเวลาในการคอมไพล์ นั่นหมายความว่าคอมไพลเลอร์ต้องสามารถประเมินและหาคำตอบได้ มีบางคนที่ต้องการให้ C # เพิ่มการสนับสนุนสำหรับคอมไพลเลอร์ที่ประเมินนิพจน์แบบไดนามิกเมื่อพบการประกาศค่าคงที่คุณลักษณะประเภทนี้จะเกี่ยวข้องกับวิธีการทำเครื่องหมายว่า "บริสุทธิ์" แต่นั่นไม่ใช่ความจริงในตอนนี้และอาจจะไม่มีเลย

หนึ่งทางเลือกในการใช้พารามิเตอร์ C # XmlReaderSettingsเริ่มต้นสำหรับวิธีการดังกล่าวจะใช้รูปแบบสุดขั้ว ในรูปแบบนี้กำหนดคลาสด้วยตัวสร้างแบบไม่มีพารามิเตอร์และคุณสมบัติที่เขียนได้แบบสาธารณะ จากนั้นแทนที่ตัวเลือกทั้งหมดด้วยค่าเริ่มต้นในวิธีการของคุณด้วยออบเจ็กต์ประเภทนี้ แม้แต่ทำให้วัตถุนี้เป็นทางเลือกโดยระบุค่าเริ่มต้นnullสำหรับวัตถุนั้น ตัวอย่างเช่น:

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

ในการเรียกใช้ไวยากรณ์แปลก ๆ เพื่อสร้างอินสแตนซ์และกำหนดคุณสมบัติทั้งหมดในนิพจน์เดียว:

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

ข้อเสีย

นี่เป็นวิธีการที่มีน้ำหนักมากในการแก้ปัญหานี้ หากคุณกำลังเขียนอินเทอร์เฟซภายในที่รวดเร็วและสกปรกและทำให้เป็นTimeSpanโมฆะและจัดการกับค่าว่างเหมือนกับค่าเริ่มต้นที่คุณต้องการจะทำงานได้ดีให้ทำเช่นนั้นแทน

นอกจากนี้หากคุณมีพารามิเตอร์จำนวนมากหรือกำลังเรียกใช้เมธอดในวง จำกัด สิ่งนี้จะมีค่าใช้จ่ายในการสร้างอินสแตนซ์คลาส แน่นอนว่าหากเรียกวิธีดังกล่าวแบบวนซ้ำมันอาจจะเป็นธรรมชาติและง่ายมากที่จะนำอินสแตนซ์ของFooSettingsวัตถุกลับมาใช้ใหม่

ประโยชน์ที่ได้รับ

ดังที่ฉันได้กล่าวไว้ในความคิดเห็นในตัวอย่างฉันคิดว่ารูปแบบนี้เหมาะสำหรับ API สาธารณะ การเพิ่มคุณสมบัติใหม่ให้กับคลาสเป็นการเปลี่ยนแปลง ABI ที่ไม่ทำลายดังนั้นคุณสามารถเพิ่มพารามิเตอร์ทางเลือกใหม่โดยไม่ต้องเปลี่ยนลายเซ็นของวิธีการของคุณโดยใช้รูปแบบนี้ทำให้โค้ดที่คอมไพล์ล่าสุดมีตัวเลือกมากขึ้นในขณะที่ยังคงรองรับโค้ดที่คอมไพล์เก่าโดยไม่ต้องทำงานเพิ่มเติม .

นอกจากนี้เนื่องจากพารามิเตอร์เมธอดเริ่มต้นที่สร้างขึ้นใน C # จะถือว่าเป็นค่าคงที่ของเวลาคอมไพล์และอบลงใน callite พารามิเตอร์เริ่มต้นจะถูกใช้โดยรหัสเมื่อมีการคอมไพล์ใหม่เท่านั้น โดยการสร้างอินสแตนซ์ออบเจ็กต์การตั้งค่าผู้เรียกจะโหลดค่าเริ่มต้นแบบไดนามิกเมื่อเรียกใช้เมธอดของคุณ ซึ่งหมายความว่าคุณสามารถอัปเดตค่าเริ่มต้นได้เพียงแค่เปลี่ยนคลาสการตั้งค่าของคุณ ดังนั้นรูปแบบนี้ช่วยให้คุณสามารถเปลี่ยนค่าเริ่มต้นได้โดยไม่ต้องคอมไพล์ผู้โทรใหม่เพื่อดูค่าใหม่หากต้องการ

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.