การใช้แฟล็กสำหรับการ "จัดกลุ่ม" ผิดหรือเปล่า


12

ความเข้าใจของฉันอยู่ที่[Flag]enums มักจะใช้สำหรับสิ่งที่สามารถนำมารวมกันที่แต่ละค่าไม่ได้พิเศษร่วมกัน

ตัวอย่างเช่น:

[Flags]
public enum SomeAttributes
{
    Foo = 1 << 0,
    Bar = 1 << 1,
    Baz = 1 << 2,
}

ใด ๆ ที่SomeAttributesคุ้มค่าจะเป็นการรวมกันของFoo, และBarBaz

ในสถานการณ์จริงที่ซับซ้อนมากขึ้นฉันใช้ enum เพื่ออธิบายDeclarationType:

[Flags]
public enum DeclarationType
{
    Project = 1 << 0,
    Module = 1 << 1,
    ProceduralModule = 1 << 2 | Module,
    ClassModule = 1 << 3 | Module,
    UserForm = 1 << 4 | ClassModule,
    Document = 1 << 5 | ClassModule,
    ModuleOption = 1 << 6,
    Member = 1 << 7,
    Procedure = 1 << 8 | Member,
    Function = 1 << 9 | Member,
    Property = 1 << 10 | Member,
    PropertyGet = 1 << 11 | Property | Function,
    PropertyLet = 1 << 12 | Property | Procedure,
    PropertySet = 1 << 13 | Property | Procedure,
    Parameter = 1 << 14,
    Variable = 1 << 15,
    Control = 1 << 16 | Variable,
    Constant = 1 << 17,
    Enumeration = 1 << 18,
    EnumerationMember = 1 << 19,
    Event = 1 << 20,
    UserDefinedType = 1 << 21,
    UserDefinedTypeMember = 1 << 22,
    LibraryFunction = 1 << 23 | Function,
    LibraryProcedure = 1 << 24 | Procedure,
    LineLabel = 1 << 25,
    UnresolvedMember = 1 << 26,
    BracketedExpression = 1 << 27,
    ComAlias = 1 << 28
}

เห็นได้ชัดว่าค่าที่กำหนดDeclarationไม่สามารถเป็นได้ทั้ง a Variableและ a LibraryProcedure- ค่าของแต่ละบุคคลไม่สามารถรวมกันได้ .. และพวกเขาไม่ได้

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

ดังนั้นฉันจึงบอกว่านี่เป็นการละเมิดธง enum - คำตอบนี้บอกว่าถ้าฉันมีชุดค่าที่ใช้กับแอปเปิ้ลและอีกชุดที่ใช้กับส้มได้ฉันต้องใช้ enum ชนิดอื่นสำหรับแอปเปิ้ลและอีกอันสำหรับส้ม - ปัญหาที่เกิดขึ้นที่นี่คือฉันต้องการการประกาศทั้งหมดที่จะมีอินเตอร์เฟซทั่วไปโดยมีDeclarationTypeการเปิดเผยในDeclarationชั้นฐาน: การมีPropertyTypeenum จะไม่เป็นประโยชน์เลย

นี่เป็นการออกแบบที่เลอะเทอะ / น่าประหลาดใจ / ไม่เหมาะสมหรือไม่? ถ้าเป็นเช่นนั้นแล้วปัญหานั้นจะแก้ไขได้อย่างไร?


พิจารณาว่าผู้คนจะใช้วัตถุประเภทDeclarationTypeอย่างไร ถ้าผมต้องการที่จะกำหนดหรือไม่xเป็นชนิดย่อยของyฉันอาจจะต้องการที่จะเขียนว่าไม่เป็นx.IsSubtypeOf(y) x && y == y
แทนเนอร์ Swett

1
@TannerSwett มันค่อนข้างจะเป็นสิ่งที่x.HasFlag(DeclarationType.Member)....
Mathieu Guindon

ใช่มันเป็นเรื่องจริง แต่ถ้าวิธีการของคุณถูกเรียกHasFlagใช้แทนIsSubtypeOfฉันก็ต้องหาวิธีอื่นในการค้นหาว่าสิ่งที่จริงหมายถึงคือ "เป็นชนิดย่อยของ" คุณสามารถสร้างวิธีขยาย แต่เป็นผู้ใช้สิ่งที่ฉันจะพบน้อยที่น่าแปลกใจคือDeclarationTypeเพียงแค่จะ struct ที่มีความIsSubtypeOfเป็นจริงวิธีการ
แทนเนอร์ Swett

คำตอบ:


10

นี่เป็นการดูหมิ่น Enums และธงอย่างแน่นอน! มันอาจใช้งานได้สำหรับคุณ แต่ใครก็ตามที่อ่านรหัสจะสับสนอย่างมาก

ถ้าฉันเข้าใจถูกต้องคุณมีการจำแนกประเภทของการประกาศตามลำดับชั้น นี่คือไกลข้อมูลมากพอที่จะเข้ารหัสใน enum เดียว แต่มีทางเลือกชัดเจน: ใช้คลาสและการสืบทอด! ดังนั้นMemberสืบทอดจากDeclarationType, PropertyสืบทอดจากMemberและอื่น ๆ

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

แก้ไข : ใน "สถานการณ์ในชีวิตจริง" ของคุณดูเหมือนว่ามีหลายสถานที่ที่พฤติกรรมถูกเลือกขึ้นอยู่กับมูลค่าของ enum นี่เป็นปฏิปักษ์จริง ๆ เนื่องจากคุณใช้switch+ enumเป็น "ความหลากหลายที่น่าสงสารม็อง" เพียงแค่เปลี่ยนค่า enum เป็นคลาสที่แตกต่างห่อหุ้มพฤติกรรมเฉพาะการประกาศและรหัสของคุณจะสะอาดขึ้นมาก


การรับมรดกเป็นความคิดที่ดี แต่คำตอบของคุณหมายถึงคลาสต่อ enum ซึ่งดูเหมือนเกินความจริง
Frank Hileman

@FrankHileman: "leafs" ในลำดับชั้นอาจแสดงเป็นค่า enum มากกว่าคลาส แต่ฉันไม่แน่ใจว่ามันจะดีกว่า ขึ้นอยู่กับว่าพฤติกรรมที่แตกต่างกันจะเชื่อมโยงกับค่า enum ที่ต่างกันหรือไม่ซึ่งในกรณีนี้คลาสที่แตกต่างจะดีกว่า
JacquesB

4
การจำแนกไม่ได้เป็นลำดับชั้นการรวมกันเป็นสถานะที่กำหนดไว้ล่วงหน้า การใช้การสืบทอดสำหรับสิ่งนี้จะเป็นการละเมิดอย่างแท้จริงการใช้คลาสในทางที่ผิด ในชั้นเรียนฉันคาดหวังอย่างน้อยข้อมูลหรือพฤติกรรมบางอย่าง พวงของคลาสที่ไม่มีอย่างใดอย่างหนึ่งคือ ... ถูกต้อง, enum
Martin Maat

เกี่ยวกับลิงค์นั้นไปยัง repo ของฉัน: พฤติกรรมต่อประเภทนั้นเกี่ยวข้องกับParserRuleContextประเภทของคลาสที่สร้างขึ้นมากกว่า enum รหัสนั้นพยายามรับตำแหน่งโทเค็นที่จะแทรกส่วนAs {Type}คำสั่งในการประกาศ; ParserRuleContextคลาสที่ได้รับการสืบทอดเหล่านี้สร้างขึ้นโดย Antr4 ต่อไวยากรณ์ที่กำหนดกฎ parser - ฉันไม่ได้ควบคุมลำดับชั้นการแยกโหนดต้นไม้ '[ค่อนข้างตื้น] ลำดับชั้นการสืบทอดแม้ว่าฉันสามารถใช้ประโยชน์จากความเป็นpartialธรรมชาติได้ ทำให้พวกเขาเปิดเผยAsTypeClauseTokenPositionคุณสมบัติบางอย่าง.. งานจำนวนมาก
Mathieu Guindon

6

ฉันพบว่าวิธีนี้ง่ายพอที่จะอ่านและเข้าใจ IMHO มันไม่ใช่เรื่องที่จะสับสน ที่ถูกกล่าวว่าฉันมีความกังวลเกี่ยวกับวิธีการนี้:

  1. การจองหลักของฉันคือไม่มีวิธีการบังคับใช้:

    เห็นได้ชัดว่าการประกาศที่กำหนดไม่สามารถเป็นทั้งตัวแปรและ LibraryProcedure - ค่าสองค่าไม่สามารถรวมกันได้ .. และพวกเขาไม่ได้

    ในขณะที่คุณไม่ได้ประกาศชุดค่าผสมข้างต้นรหัสนี้

    var oops = DeclarationType.Variable | DeclarationType.LibraryProcedure;

    ถูกต้องสมบูรณ์ และไม่มีวิธีที่จะตรวจจับข้อผิดพลาดเหล่านั้นในเวลารวบรวม

  2. มีการ จำกัด จำนวนข้อมูลที่คุณสามารถเข้ารหัสในแฟล็กบิตซึ่งคืออะไร 64 บิต สำหรับตอนนี้คุณกำลังใกล้จะถึงขนาดอันตรายintแล้วและถ้า Enum นี้เติบโตขึ้นเรื่อย ๆ ในที่สุดคุณก็อาจจะหมดบิต ...

บรรทัดล่างคือฉันคิดว่านี่เป็นวิธีที่ถูกต้อง แต่ฉันลังเลที่จะใช้มันสำหรับลำดับชั้นขนาดใหญ่ / ซับซ้อนของธง


ดังนั้นการทดสอบหน่วย parser อย่างละเอียดจะอยู่ # 1 แล้ว FWIW enum เริ่มต้นเมื่อไม่ได้ระบุค่ามาตรฐาน enum เมื่อประมาณ 3 ปีที่แล้ว .. หลังจากเหนื่อยล้าจากการตรวจสอบค่าเฉพาะบางอย่าง (เช่นในการตรวจสอบโค้ด) ที่ enum ตั้งค่าไว้ รายการไม่ได้เติบโตขึ้นเมื่อเวลาผ่านไปเช่นกัน แต่intความสามารถในการเอาชนะคือความกังวลที่แท้จริง
Mathieu Guindon

3

TL; DRเลื่อนไปที่ด้านล่างสุด


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

ในสถานการณ์เฉพาะนี้มีพฤติกรรม polymorphic น้อยมากระหว่างโหนดประเภทต่าง ๆ กล่าวอีกนัยหนึ่งในขณะที่มันจำเป็นสำหรับต้นไม้ที่จะสามารถบรรจุโหนดที่แตกต่างกันมาก (พันธุ์), การเยี่ยมชมที่แท้จริงของโหนดเหล่านี้โดยทั่วไปจะหันไปใช้โซ่ยักษ์ถ้า - แล้ว - อื่นโซ่ (หรือinstanceof/ isตรวจสอบ) เช็คขนาดยักษ์เหล่านี้จะเกิดขึ้นในหลาย ๆ ที่ทั่วทั้งโครงการ นี่คือเหตุผลที่ enums อาจดูเหมือนว่ามีประโยชน์หรืออย่างน้อยก็มีประโยชน์เหมือนกับinstanceof/ isเช็ค

รูปแบบของผู้เข้าชมอาจยังมีประโยชน์ ในคำอื่น ๆ instanceofที่มีรูปแบบการเขียนโปรแกรมต่างๆที่สามารถนำมาใช้แทนของห่วงโซ่ยักษ์ใหญ่ของ อย่างไรก็ตามหากคุณต้องการพูดคุยเกี่ยวกับประโยชน์และข้อเสียต่าง ๆ คุณจะต้องแสดงตัวอย่างรหัสจากห่วงโซ่ที่น่าเกลียดที่สุดของinstanceofโครงการแทนที่จะเป็นการพูดคลุมเครือเกี่ยวกับ enums

นี่ไม่ได้เป็นการบอกว่าคลาสและลำดับชั้นการสืบทอดไม่เป็นประโยชน์ ค่อนข้างตรงกันข้าม ในขณะที่ไม่มีพฤติกรรม polymorphic ใด ๆ ที่ทำงานข้ามการประกาศทุกประเภท (นอกเหนือจากข้อเท็จจริงที่ว่าการประกาศทุกครั้งจะต้องมีNameคุณสมบัติ) แต่ก็มีพฤติกรรม polymorphic มากมายที่แบ่งปันโดยพี่น้องในบริเวณใกล้เคียง ตัวอย่างเช่นFunctionและProcedureอาจแบ่งปันพฤติกรรมบางอย่าง (ทั้งเรียกได้และยอมรับรายการของอาร์กิวเมนต์ที่พิมพ์) และPropertyGetจะสืบทอดพฤติกรรมจากFunction(ทั้งสองมีReturnType) คุณอาจใช้ enums หรือการตรวจสอบการสืบทอดสำหรับห่วงโซ่ if-then-else ยักษ์ แต่พฤติกรรม polymorphic แต่การแยกส่วนจะยังคงต้องดำเนินการในชั้นเรียน

มีคำแนะนำออนไลน์จำนวนมากกับมากเกินไปของคุณinstanceof/ isตรวจสอบ ประสิทธิภาพไม่ได้เป็นหนึ่งในเหตุผล เหตุผลก็คือเพื่อป้องกันโปรแกรมเมอร์จากการค้นพบพฤติกรรม polymorphic ที่เหมาะสมอย่างเป็นธรรมชาติเช่นถ้าinstanceof/ isเป็นไม้ยันรักแร้ แต่ในสถานการณ์ของคุณคุณไม่มีทางเลือกอื่นเนื่องจากโหนดเหล่านี้มีเหมือนกันน้อยมาก

ตอนนี้นี่คือคำแนะนำที่เป็นรูปธรรม


มีหลายวิธีในการแสดงการจัดกลุ่มที่ไม่ใช่ใบไม้


เปรียบเทียบส่วนที่ตัดตอนมาของรหัสต้นฉบับของคุณ ...

[Flags]
public enum DeclarationType
{
    Member = 1 << 7,
    Procedure = 1 << 8 | Member,
    Function = 1 << 9 | Member,
    Property = 1 << 10 | Member,
    PropertyGet = 1 << 11 | Property | Function,
    PropertyLet = 1 << 12 | Property | Procedure,
    PropertySet = 1 << 13 | Property | Procedure,
    LibraryFunction = 1 << 23 | Function,
    LibraryProcedure = 1 << 24 | Procedure,
}

เป็นเวอร์ชั่นที่มีการปรับเปลี่ยนนี้:

[Flags]
public enum DeclarationType
{
    Nothing = 0, // to facilitate bit testing

    // Let's assume Member is not a concrete thing, 
    // which means it doesn't need its own bit
    /* Member = 1 << 7, */

    // Procedure and Function are concrete things; meanwhile 
    // they can still have sub-types.
    Procedure = 1 << 8, 
    Function = 1 << 9, 
    Property = 1 << 10,

    PropertyGet = 1 << 11,
    PropertyLet = 1 << 12,
    PropertySet = 1 << 13,

    LibraryFunction = 1 << 23,
    LibraryProcedure = 1 << 24,

    // new
    Procedures = Procedure | PropertyLet | PropertySet | LibraryProcedure,
    Functions = Function | PropertyGet | LibraryFunction,
    Properties = PropertyGet | PropertyLet | PropertySet,
    Members = Procedures | Functions | Properties,
    LibraryMembers = LibraryFunction | LibraryProcedure 
}

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

มีข้อแม้: ถ้ามีประเภทการประกาศนามธรรมที่มีลูกคนเดียวและหากมีความต้องการที่จะแยกแยะความแตกต่างระหว่างนามธรรมหนึ่ง (ผู้ปกครอง) จากคอนกรีตหนึ่ง (เด็ก) จากนั้นหนึ่งนามธรรมจะยังคงต้องการบิตของตัวเอง .


หนึ่งข้อแม้ที่เฉพาะเจาะจงสำหรับคำถามนี้: a Propertyเริ่มต้นเป็นตัวระบุ (เมื่อคุณเพิ่งเห็นชื่อโดยไม่เห็นว่ามันถูกใช้ในรหัส) แต่มันอาจแปลงร่างเป็นPropertyGet/ PropertyLet/ PropertySetทันทีที่คุณเห็นว่ามันถูกใช้อย่างไร ในรหัส กล่าวอีกนัยหนึ่งในขั้นตอนการแยกวิเคราะห์ที่แตกต่างกันคุณอาจจำเป็นต้องทำเครื่องหมายPropertyตัวระบุว่าเป็น "ชื่อนี้หมายถึงสถานที่ให้บริการ" และในภายหลังเปลี่ยนเป็น "บรรทัดของรหัสนี้คือการเข้าถึงคุณสมบัตินี้ด้วยวิธีใดวิธีหนึ่ง"

ในการแก้ไขข้อแม้นี้คุณอาจต้องใช้ enums สองชุด หนึ่ง enum หมายถึงสิ่งที่ชื่อ (ตัวระบุ) คือ; อีก enum หมายถึงสิ่งที่รหัสพยายามทำ (เช่นการประกาศเนื้อความของบางสิ่งบางอย่างพยายามใช้บางอย่างในวิธีที่แน่นอน)


พิจารณาว่าข้อมูลเสริมเกี่ยวกับแต่ละค่า enum สามารถอ่านได้จากอาเรย์แทนหรือไม่

คำแนะนำนี้ไม่สามารถเกิดขึ้นพร้อมกันกับคำแนะนำอื่น ๆ ได้เนื่องจากต้องการการแปลงค่า powers-of-two กลับไปเป็นค่าจำนวนเต็มแบบไม่ลบ

public enum DeclarationType
{
    Procedure = 8,
    Function = 9,
    Property = 10,
    PropertyGet = 11,
    PropertyLet = 12,
    PropertySet = 13,
    LibraryFunction = 23,
    LibraryProcedure = 24,
}

static readonly bool[] DeclarationTypeIsMember = new bool[32]
{
    ?, ?, ?, ?, ?, ?, ?, ?,                   // bit[0] ... bit[7]
    true, true, true, true, true, true, ?, ?, // bit[8] ... bit[15]
    ?, ?, ?, ?, ?, ?, ?, true,                // bit[16] ... bit[23]
    true, ...                                 // bit[24] ... 
}

static bool IsMember(DeclarationType dt)
{
    int intValue = (int)dt;
    return (intValue < 0 || intValue >= 32) ? false : DeclarationTypeIsMember[intValue];
    // you can also throw an exception if the enum is outside range.
}

// likewise for IsFunction(dt), IsProcedure(dt), IsProperty(dt), ...

การบำรุงรักษาจะเป็นปัญหาได้


ตรวจสอบว่าการแมปแบบหนึ่งต่อหนึ่งระหว่างประเภท C # (คลาสในลำดับชั้นการสืบทอด) และค่า enum ของคุณ

(หรืออีกวิธีหนึ่งคุณสามารถปรับค่า enum ของคุณเพื่อให้แน่ใจว่าการแมปแบบหนึ่งต่อหนึ่งพร้อมกับประเภท)

ใน C # ห้องสมุดจำนวนมากละเมิดType object.GetType()วิธีการที่ดีสำหรับดีหรือไม่ดี

ทุกที่ที่คุณเก็บค่า enum เป็นค่าคุณอาจถามตัวเองว่าคุณสามารถเก็บTypeค่าเป็นค่าได้หรือไม่

ในการใช้เคล็ดลับนี้คุณสามารถเริ่มต้นตารางแฮชแบบอ่านอย่างเดียวสองตาราง ได้แก่ :

// For disambiguation, I'll assume that the actual 
// (behavior-implementing) classes are under the 
// "Lang" namespace.

static readonly Dictionary<Type, DeclarationType> TypeToDeclEnum = ... 
{
    { typeof(Lang.Procedure), DeclarationType.Procedure },
    { typeof(Lang.Function), DeclarationType.Function },
    { typeof(Lang.Property), DeclarationType.Property },
    ...
};

static readonly Dictionary<DeclarationType, Type> DeclEnumToType = ...
{
    // same as the first dictionary; 
    // just swap the key and the value
    ...
};

การป้องกันขั้นสุดท้ายสำหรับผู้ที่แนะนำคลาสและลำดับชั้นการสืบทอด ...

เมื่อคุณเห็นว่าenums เป็นการประมาณลำดับชั้นการสืบทอดคำแนะนำต่อไปนี้จะเก็บ:

  • ออกแบบ (หรือปรับปรุง) ลำดับชั้นการสืบทอดของคุณก่อน
  • จากนั้นย้อนกลับและออกแบบ enums ของคุณเพื่อประมาณลำดับชั้นการสืบทอดนั้น

โครงการนี้เป็น VBIDE Add-in จริง ๆ แล้วฉันกำลังวิเคราะห์และวิเคราะห์โค้ด VBA =)
Mathieu Guindon

1

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

ธงเป็นเครื่องมือในการส่งสัญญาณสถานะของการคัดเลือก ถ้าฉันอยากรู้ว่ามีบางอย่างเป็นผลไม้ฉันก็หา

thingy & Organic.Fruit! = 0

อ่านได้มากกว่า

thingy & (Organic.Apple | Organic.Orange | Organic.Pear)! = 0

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

ให้คะแนนบราวนี่กับผู้ชายคนนี้!


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