เตรียมใช้งานคลาสฟิลด์ในตัวสร้างหรือการประกาศ?


413

ฉันเพิ่งเขียนโปรแกรมใน C # และ Java เมื่อเร็ว ๆ นี้และฉันอยากรู้ว่าที่ที่ดีที่สุดคือการเริ่มต้นฟิลด์คลาสของฉัน

ฉันควรทำอย่างไรเมื่อมีการประกาศ:

public class Dice
{
    private int topFace = 1;
    private Random myRand = new Random();

    public void Roll()
    {
       // ......
    }
}

หรือในตัวสร้าง:

public class Dice
{
    private int topFace;
    private Random myRand;

    public Dice()
    {
        topFace = 1;
        myRand = new Random();
    }

    public void Roll()
    {
        // .....
    }
}

ฉันสงสัยจริงๆว่าทหารผ่านศึกบางคนคิดว่าวิธีปฏิบัติที่ดีที่สุดคืออะไร ฉันต้องการที่จะสอดคล้องและยึดมั่นในวิธีการหนึ่ง


3
โปรดทราบว่าสำหรับ structs คุณไม่สามารถมี initializers ฟิลด์อินสแตนซ์ดังนั้นคุณจึงไม่มีทางเลือกนอกจากใช้ constructor
yoyo

เนื่องจากคุณนำเสนอ "แนวปฏิบัติที่ดีที่สุด": satisfice.com/blog/archives/5164 , forbes.com/sites/mikemyatt/2012/08/15/best-practices-arent/ …
Stephen C

คำตอบ:


310

กฎของฉัน:

  1. อย่าเริ่มต้นด้วยค่าเริ่มต้นในการประกาศ ( null, false, 0, 0.0... )
  2. ต้องการกำหนดค่าเริ่มต้นในการประกาศหากคุณไม่มีพารามิเตอร์ตัวสร้างที่เปลี่ยนแปลงค่าของเขตข้อมูล
  3. หากค่าของฟิลด์เปลี่ยนแปลงเนื่องจากพารามิเตอร์ Constructor ให้กำหนดค่าเริ่มต้นใน Constructor
  4. มีความสอดคล้องในการปฏิบัติของคุณ (กฎที่สำคัญที่สุด)

4
ฉันคาดหวังว่า kokos หมายความว่าคุณไม่ควรเริ่มต้นสมาชิกให้เป็นค่าเริ่มต้นของพวกเขา (0, เท็จ, เป็นโมฆะ ฯลฯ ) เนื่องจากคอมไพเลอร์จะทำเช่นนั้นสำหรับคุณ (1. ) แต่ถ้าคุณต้องการเริ่มต้นเขตข้อมูลให้เป็นอย่างอื่นนอกเหนือจากค่าเริ่มต้นคุณควรทำในการประกาศ (2. ) ฉันคิดว่ามันอาจเป็นการใช้คำว่า "ค่าเริ่มต้น" ที่ทำให้คุณสับสน
Ricky Helgesson

95
ฉันไม่เห็นด้วยกับกฎที่ 1 - โดยไม่ระบุค่าเริ่มต้น (ไม่ว่าจะเริ่มต้นโดยคอมไพเลอร์หรือไม่) คุณกำลังปล่อยให้นักพัฒนาคาดเดาหรือไปหาเอกสารเกี่ยวกับค่าเริ่มต้นสำหรับภาษานั้น ๆ เพื่อวัตถุประสงค์ในการอ่านได้ฉันจะระบุค่าเริ่มต้นเสมอ
James

32
ค่าเริ่มต้นของประเภทdefault(T)อยู่เสมอค่าที่มีแทน binary 0ภายในของ
Olivier Jacot-Descombes

15
ไม่ว่าคุณจะชอบกฎที่ 1 จะไม่สามารถใช้กับเขตข้อมูลแบบอ่านอย่างเดียวได้ซึ่งจะต้องเริ่มต้นอย่างชัดเจนโดยเวลาที่ตัวสร้างสร้างเสร็จ
yoyo

36
ฉันไม่เห็นด้วยกับผู้ที่ไม่เห็นด้วยกับกฎที่ 1 มันโอเคที่จะคาดหวังให้คนอื่นเรียนรู้ภาษา C # เช่นเดียวกับที่เราไม่แสดงความคิดเห็นแต่ละforeachวงด้วย "นี่จะทำซ้ำสิ่งต่อไปนี้สำหรับรายการทั้งหมดในรายการ" เราไม่จำเป็นต้องปรับค่าเริ่มต้นของ C # เป็นประจำ นอกจากนี้เราไม่จำเป็นต้องแสร้งว่า C # มีความหมายไม่เจาะจง เนื่องจากการที่ไม่มีค่านั้นมีความหมายชัดเจนจึงเป็นการดีที่จะปล่อยทิ้งไว้ หากเหมาะสมอย่างชัดเจนคุณควรใช้newเมื่อสร้างผู้รับมอบสิทธิ์ใหม่เสมอ(ตามที่ต้องการใน C # 1) แต่ใครทำอย่างนั้น? ภาษาได้รับการออกแบบมาสำหรับผู้เขียนโค้ดที่ขยันขันแข็ง
Edward Brey

149

ใน C # มันไม่สำคัญ ตัวอย่างโค้ดสองตัวอย่างที่คุณให้นั้นมีความเทียบเท่าอย่างเต็มที่ ในตัวอย่างแรกคอมไพเลอร์ C # (หรือมันคือ CLR?) จะสร้างคอนสตรัคเตอร์เปล่าและกำหนดค่าเริ่มต้นตัวแปรราวกับว่าพวกเขาอยู่ในคอนสตรัคเตอร์ (มีความแตกต่างเล็กน้อยในเรื่องนี้ที่ Jon Skeet อธิบายไว้ในข้อคิดเห็นด้านล่าง) หากมีคอนสตรัคเตอร์อยู่แล้วการเริ่มต้น "เหนือ" ใด ๆ จะถูกย้ายไปที่ด้านบนของมัน

ในแง่ของแนวปฏิบัติที่ดีที่สุดอดีตนั้นเกิดข้อผิดพลาดน้อยกว่าคนหลังเนื่องจากมีคนสามารถเพิ่มตัวสร้างอื่นได้ง่ายและลืมที่จะโยงมัน


4
ไม่ถูกต้องหากคุณเลือกเริ่มต้นคลาสด้วย GetUninitializedObject สิ่งที่อยู่ใน ctor จะไม่ได้สัมผัส แต่จะมีการประกาศฟิลด์
Wolf5

28
ที่จริงมันก็สำคัญ ถ้าตัวสร้างคลาสพื้นฐานเรียกใช้เมธอดเสมือน (ซึ่งโดยทั่วไปเป็นความคิดที่ไม่ดี แต่สามารถเกิดขึ้นได้) ซึ่งถูกแทนที่ในคลาสที่ได้รับจากนั้นใช้การเริ่มต้นตัวแปรอินสแตนซ์ตัวแปรจะเริ่มต้นเมื่อวิธีการที่เรียกว่า คอนสตรัคพวกเขาจะไม่เป็น (อินสแตนซ์ตัวแปร initializers ถูกเรียกใช้งานก่อนที่ตัวสร้างคลาสพื้นฐานจะถูกเรียกใช้)
Jon Skeet

2
จริงๆ? ฉันสาบานว่าฉันคว้าข้อมูลนี้จาก CLR ของ Richter ผ่าน C # (รุ่นที่ 2 ที่ฉันคิด) และส่วนสำคัญของมันคือว่านี่คือน้ำตาล syntactic (ฉันอาจได้อ่านมันผิด?) และ CLR เพียงแค่ติดขัดตัวแปรลงในตัวสร้าง แต่คุณกำลังระบุว่านี่ไม่ใช่กรณีเช่นการเริ่มต้นสมาชิกสามารถเริ่มต้นก่อนการเริ่มต้น ctor ในสถานการณ์ที่บ้าคลั่งของการโทรเสมือนใน ctor พื้นฐานและมีการแทนที่ในชั้นเรียนที่มีปัญหา ฉันเข้าใจถูกต้องหรือไม่ คุณเพิ่งค้นพบสิ่งนี้? งงงวยกับความคิดเห็นที่อัปเดตนี้ในโพสต์เก่า 5 ปี (OMG เป็นเวลา 5 ปีแล้วหรือยัง)
Quibblesome

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

1
@Quibblesome อดีตจะเป็นปัญหาหากมีการกำหนดค่าและส่งผ่านไปยังวิธีการ WCF ซึ่งจะรีเซ็ตข้อมูลเป็นชนิดข้อมูลเริ่มต้นของโมเดล วิธีปฏิบัติที่ดีที่สุดคือทำการเริ่มต้นในตัวสร้าง (หนึ่งตัวในภายหลัง)
Pranesh Janarthanan

16

ความหมายของ C # แตกต่างจาก Java ที่นี่เล็กน้อย ในการกำหนด C # ในการประกาศจะดำเนินการก่อนที่จะเรียกตัวสร้าง superclass ใน Java จะทำทันทีหลังจากที่อนุญาตให้ใช้ 'this' (มีประโยชน์อย่างยิ่งสำหรับคลาสภายในที่ไม่ระบุชื่อ) และหมายความว่า semantics ของทั้งสองรูปแบบตรงกันจริงๆ

หากเป็นไปได้ให้ตั้งค่าฟิลด์สุดท้าย


15

ฉันคิดว่ามีหนึ่งข้อแม้ ฉันเคยทำผิดพลาดดังกล่าว: ภายในชั้นเรียนที่ได้รับฉันพยายามที่จะ "เริ่มต้นที่ประกาศ" เขตข้อมูลที่ได้รับมาจากชั้นฐานนามธรรม ผลลัพธ์คือมีสองชุดเขตข้อมูลหนึ่งคือ "ฐาน" และอีกชุดเป็นฟิลด์ประกาศใหม่และใช้เวลาพอสมควรในการดีบัก

บทเรียน: เพื่อเริ่มต้นเขตข้อมูลที่สืบทอดมาคุณจะต้องทำมันภายในตัวสร้าง


ดังนั้นถ้าคุณอ้างถึงเขตข้อมูลด้วยderivedObject.InheritedFieldมันจะอ้างอิงถึงฐานของคุณหรือที่ได้รับมา?
RayLuo

6

สมมติว่าประเภทในตัวอย่างของคุณแน่นอนต้องการเริ่มต้นเขตข้อมูลในตัวสร้าง กรณีพิเศษคือ:

  • ฟิลด์ในคลาส / เมธอดแบบคงที่
  • ฟิลด์ที่พิมพ์เป็นแบบสแตติก / สุดท้าย / et al

ฉันมักจะคิดว่ารายการฟิลด์ที่ด้านบนของชั้นเรียนเป็นสารบัญ (สิ่งที่อยู่ในที่นี้ไม่ใช่วิธีการใช้) และตัวสร้างเป็นบทนำ วิธีการเรียนการสอนเป็นบท


ทำไม "แน่นอน" เหรอ? คุณให้การตั้งค่าสไตล์เท่านั้นโดยไม่อธิบายเหตุผล โอ้รอไม่เป็นไรตามคำตอบของ @quibblesome พวกเขาเป็น "เทียบเท่าอย่างเต็มที่" ดังนั้นจึงเป็นเพียงการตั้งค่าสไตล์ส่วนตัว
RayLuo

4

ถ้าฉันบอกคุณมันขึ้นอยู่กับว่า?

โดยทั่วไปแล้วฉันจะเริ่มต้นทุกอย่างและทำอย่างสอดคล้องกัน ใช่มันชัดเจนเกินไป แต่ก็ง่ายกว่าเล็กน้อยในการรักษา

หากเรากังวลเกี่ยวกับประสิทธิภาพการทำงานดีฉันเริ่มต้นเฉพาะสิ่งที่ต้องทำและวางไว้ในพื้นที่ที่มันให้ผลสูงสุดสำหรับเจ้าชู้

ในระบบเรียลไทม์ฉันถามว่าฉันต้องการตัวแปรหรือค่าคงที่หรือไม่

และใน C ++ ฉันมักจะทำถัดจากไม่มีการเริ่มต้นในสถานที่อย่างใดอย่างหนึ่งและย้ายไปไว้ในฟังก์ชั่น Init () ทำไม? ใน C ++ ถ้าคุณกำลังเริ่มต้นบางสิ่งบางอย่างที่อาจทำให้เกิดข้อยกเว้นระหว่างการสร้างวัตถุที่คุณเปิดตัวเองให้กับหน่วยความจำรั่ว


4

มีสถานการณ์มากมายและหลากหลาย

ฉันต้องการรายการที่ว่างเปล่า

สถานการณ์มีความชัดเจน ฉันแค่ต้องเตรียมรายชื่อของฉันและป้องกันข้อยกเว้นจากการถูกโยนทิ้งเมื่อมีคนเพิ่มรายการลงในรายการ

public class CsvFile
{
    private List<CsvRow> lines = new List<CsvRow>();

    public CsvFile()
    {
    }
}

ฉันรู้ค่า

ฉันรู้อย่างแน่นอนว่าค่าใดที่ฉันต้องการโดยปริยายหรือฉันต้องใช้ตรรกะอื่น

public class AdminTeam
{
    private List<string> usernames;

    public AdminTeam()
    {
         usernames = new List<string>() {"usernameA", "usernameB"};
    }
}

หรือ

public class AdminTeam
{
    private List<string> usernames;

    public AdminTeam()
    {
         usernames = GetDefaultUsers(2);
    }
}

รายการที่ว่างเปล่าที่มีค่าที่เป็นไปได้

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

public class AdminTeam
{
    private List<string> usernames = new List<string>();

    public AdminTeam()
    {
    }

    public AdminTeam(List<string> admins)
    {
         admins.ForEach(x => usernames.Add(x));
    }
}

3

ใน Java, initializer ที่มีการประกาศหมายถึงฟิลด์นั้นจะถูก initialize ด้วยวิธีเดียวกันเสมอไม่ว่าจะใช้ Constructor ใด (ถ้าคุณมีมากกว่าหนึ่ง) หรือพารามิเตอร์ของ Constructor ของคุณ (ถ้ามีอาร์กิวเมนต์) แม้ว่า Construct อาจจะสร้างในภายหลัง เปลี่ยนค่า (ถ้าไม่ใช่ค่าสุดท้าย) ดังนั้นการใช้ initializer ด้วยการประกาศแนะนำให้ผู้อ่านว่าค่าเริ่มต้นคือค่าที่สนามมีในทุกกรณีโดยไม่คำนึงถึงคอนสตรัคที่ใช้และไม่คำนึงถึงพารามิเตอร์ที่ส่งผ่านไปยังตัวสร้างใด ๆ ดังนั้นให้ใช้ initializer พร้อมกับการประกาศหากและเสมอถ้าค่าสำหรับวัตถุที่สร้างขึ้นทั้งหมดนั้นเหมือนกัน


3

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

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

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

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


2

ความสอดคล้องเป็นสิ่งสำคัญ แต่เป็นคำถามที่ถามตัวเองว่า: "ฉันมีคอนสตรัคเตอร์สำหรับสิ่งอื่นหรือไม่?"

โดยทั่วไปแล้วฉันกำลังสร้างแบบจำลองสำหรับการถ่ายโอนข้อมูลที่คลาสตัวเองไม่ทำอะไรเลยยกเว้นทำงานเป็นที่อยู่อาศัยสำหรับตัวแปร

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

ดังที่คนอื่น ๆ ได้กล่าวขึ้นอยู่กับการใช้งานของคุณ ทำให้มันง่ายและอย่าทำอะไรเพิ่มเติมที่คุณไม่ต้องทำ


2

พิจารณาสถานการณ์ที่คุณมีตัวสร้างมากกว่าหนึ่งคน การเริ่มต้นจะแตกต่างกันสำหรับตัวสร้างที่แตกต่างกันหรือไม่? หากพวกเขาจะเหมือนกันแล้วทำไมซ้ำสำหรับตัวสร้างแต่ละตัว? สิ่งนี้สอดคล้องกับคำสั่ง kokos แต่อาจไม่เกี่ยวข้องกับพารามิเตอร์ ตัวอย่างเช่นคุณต้องการเก็บค่าสถานะซึ่งแสดงวิธีการสร้างวัตถุ จากนั้นแฟล็กนั้นจะเริ่มต้นแตกต่างกันสำหรับตัวสร้างที่แตกต่างกันโดยไม่คำนึงถึงพารามิเตอร์ตัวสร้าง ในทางตรงกันข้ามถ้าคุณทำซ้ำการเริ่มต้นที่เหมือนกันสำหรับแต่ละตัวสร้างคุณปล่อยให้เป็นไปได้ว่าคุณ (โดยไม่ได้ตั้งใจ) เปลี่ยนพารามิเตอร์การเริ่มต้นในบางส่วนของการก่อสร้าง แต่ไม่ได้อยู่ในคนอื่น ดังนั้นแนวคิดพื้นฐานที่นี่คือรหัสทั่วไปควรมีตำแหน่งทั่วไปและอาจไม่ซ้ำในสถานที่ต่างกัน


1

มีประโยชน์ด้านประสิทธิภาพเล็กน้อยในการตั้งค่าในการประกาศ หากคุณตั้งไว้ในตัวสร้างมันจะถูกตั้งค่าจริงสองครั้ง (แรกเป็นค่าเริ่มต้นจากนั้นรีเซ็ตใน ctor)


2
ใน C # ฟิลด์จะตั้งค่าเริ่มต้นเสมอ การปรากฏตัวของ initializer ไม่สร้างความแตกต่าง
Jeffrey L Whitledge

0

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

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


0

ไม่ใช่คำตอบที่ตรงกับคำถามของคุณเกี่ยวกับแนวปฏิบัติที่ดีที่สุดแต่จุดทบทวนที่สำคัญและเกี่ยวข้องคือในกรณีของคำจำกัดความของคลาสทั่วไปให้วางไว้บนคอมไพเลอร์เพื่อเริ่มต้นด้วยค่าเริ่มต้นหรือเราต้องใช้วิธีพิเศษเพื่อเริ่มต้นฟิลด์ เป็นค่าเริ่มต้น (ถ้าจำเป็นอย่างยิ่งสำหรับความสามารถในการอ่านโค้ด)

class MyGeneric<T>
{
    T data;
    //T data = ""; // <-- ERROR
    //T data = 0; // <-- ERROR
    //T data = null; // <-- ERROR        

    public MyGeneric()
    {
        // All of the above errors would be errors here in constructor as well
    }
}

และวิธีพิเศษในการเริ่มต้นเขตข้อมูลทั่วไปเป็นค่าเริ่มต้นของมันคือต่อไปนี้:

class MyGeneric<T>
{
    T data = default(T);

    public MyGeneric()
    {           
        // The same method can be used here in constructor
    }
}

0

เมื่อคุณไม่ต้องการตรรกะหรือการจัดการข้อผิดพลาด:

  • เริ่มต้นเขตข้อมูลชั้นที่ประกาศ

เมื่อคุณต้องการการจัดการตรรกะหรือข้อผิดพลาด:

  • เริ่มต้นเขตข้อมูลคลาสในตัวสร้าง

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

จากhttps://docs.oracle.com/javase/tutorial/java/javaOO/initial.html

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