Mike Torrettinni 198 Posted November 12, 2019 Not sure how to frame the question title, so I hope it makes sense what I'm trying to ask: I have many examples of defined values and I use one of the 2 approaches below. Is there a better approach, or it is what it is: For example: I have Projects that are defined with 3 values: enum, internal_id (default value from external data) and caption (to show user). So, here is 1st approach that I use: type TProjectType = (ptMain, ptSub, ptExternalDev, ptInternalDev); const cProjDefValues: array[TProjectType] of string = ('main_proj', 'sub_proj', 'dev_ext', 'dev_int'); cProjCaptions: array[TProjectType] of string = ('Main Project', 'Sub Project', 'External Dev Project', 'Internal Dev Project'); And then I just use TProjectType to retrieve DefValue or Caption, whatever I need from consts. I like this approach, but the problem is for example where I have 20+ enums - in this case the code because a long messy, not quickly readable, since each has to have 20+ values. And here is another example that I sometimes use, 2nd example: type TProjectType = (ptMain, ptSub, ptExternalDev, ptInternalDev); TProject = record ProjectType: TProjectType; DefValue: string; Caption: string; constructor New(const aType: TProjectType; const aDefValue, aCaption: string); end; Projects: TList<TProject>; constructor TProject.New(const aType: TProjectType; const aDefValue: string; const aCaption: string); begin ProjectType := aType; DefValue := aDefValue; Caption := aCaption; end; procedure TForm7.FormCreate(Sender: TObject); begin Projects := TList<TProject>.Create; Projects.Add(TProject.New(ptMain, 'main_proj', 'Project')); Projects.Add(TProject.New(ptSub, 'sub_proj', 'Sub Project')); Projects.Add(TProject.New(ptExternalDev, 'dev_ext', 'External Dev Project')); Projects.Add(TProject.New(ptInternalDev, 'dev_int', 'Internal Dev Project')); end; I like this approach, because I have all definitions in single method, even if I have 100+ records, it's still in 1 method. But I need to run this method at the start, while 1st example just exists, without creating any data. So, what I'm looking for is any kind of comment, advice what is better, is there a third way (even better than these example). Share this post Link to post
Henk Post 3 Posted November 12, 2019 (edited) The first approach ensures a single caption and defvalue for each value of tprojecttype due to the array structure. With the list approach you could add multiple entries for one value of tprojecttype. You could replace the TList with a TDictionary. This way you can easily access the strings attached to the specific value of TProjectType. A third solution that I prefer is the use of a record helper for TProjectType. Then you can use ptMain.Caption and ptSub.DefValue. The additional strings are closely connected to TProjectType. Edited November 12, 2019 by Henk Post 1 Share this post Link to post
David Heffernan 2345 Posted November 12, 2019 I use attributes attached to enumerated types to handle this 1 1 Share this post Link to post
Fr0sT.Brutal 900 Posted November 12, 2019 I prefer the 1st. But keep in mind that most of hard-coded constants could require changing to run-time assignments if you want multi-language support 1 Share this post Link to post
Mike Torrettinni 198 Posted November 12, 2019 Wow, so many different implementations, I had a problem choosing between the 2 I knew of 🙂 Share this post Link to post
Mike Torrettinni 198 Posted November 12, 2019 5 hours ago, David Heffernan said: I use attributes attached to enumerated types to handle this Are you talking about TCustomAttribute, like this answer: https://stackoverflow.com/questions/2134120/delphi-2010-rtti-explore-enumerations/2620883#2620883 Share this post Link to post
Lars Fosdal 1792 Posted November 12, 2019 I prefer record helpers, combined with the const arrays. I really wish I could make generic record helpers for arrays, though 😕 type TProjectType = (ptMain, ptSub, ptExternalDev, ptInternalDev); TProjectTypeHelper = record helper for TProjectType private const cProjDefValues: array[TProjectType] of string = ('main_proj', 'sub_proj', 'dev_ext', 'dev_int'); cProjCaptions: array[TProjectType] of string = ('Main Project', 'Sub Project', 'External Dev Project', 'Internal Dev Project'); public class function DefValueOf(const aProjectType: TProjectType): string; static; class function CaptionOf(const aProjectType: TProjectType): string; static; function DefValue: string; function Caption: string; end; implementation { TProjectTypeHelper } function TProjectTypeHelper.Caption: string; begin Result := CaptionOf(Self); end; class function TProjectTypeHelper.CaptionOf(const aProjectType: TProjectType): string; begin Result := cProjCaptions[aProjectType]; end; function TProjectTypeHelper.DefValue: string; begin Result := DefValueOf(Self); end; class function TProjectTypeHelper.DefValueOf(const aProjectType: TProjectType): string; begin Result := cProjDefValues[aProjectType]; end; The class functions could also be case statements - but the array[type] ensures you have values for all. The weak spot here is if you insert a value in the constant list, or reorder values - without doing the same for the strings. Or you can use class vars - which would make it easier to load up different languages at runtime, if so needed. type TProjectType = (ptMain, ptSub, ptExternalDev, ptInternalDev); TProjectTypeHelper = record helper for TProjectType private class var cProjDefValues: array[TProjectType] of string; cProjCaptions: array[TProjectType] of string; public class procedure InitHelper; static; class function DefValueOf(const aProjectType: TProjectType): string; static; class function CaptionOf(const aProjectType: TProjectType): string; static; function DefValue: string; function Caption: string; end; implementation { TProjectTypeHelper } function TProjectTypeHelper.Caption: string; begin Result := CaptionOf(Self); end; class function TProjectTypeHelper.CaptionOf(const aProjectType: TProjectType): string; begin Result := cProjCaptions[aProjectType]; end; function TProjectTypeHelper.DefValue: string; begin Result := DefValueOf(Self); end; class function TProjectTypeHelper.DefValueOf(const aProjectType: TProjectType): string; begin Result := cProjDefValues[aProjectType]; end; class procedure TProjectTypeHelper.InitHelper; begin cProjDefValues[ptMain] := 'main_proj'; cProjCaptions [ptMain] := 'Main Project'; cProjDefValues[ptSub] := 'sub_proj'; cProjCaptions [ptSub] := 'Sub Project'; cProjDefValues[ptExternalDev] := 'dev_ext'; cProjCaptions [ptExternalDev] := 'External Dev Project'; cProjDefValues[ptInternalDev] := 'dev_int'; cProjCaptions [ptInternalDev] := 'Internal Dev Project'; end; 1 Share this post Link to post
Mike Torrettinni 198 Posted November 12, 2019 30 minutes ago, Lars Fosdal said: I prefer record helpers, combined with the const arrays. I really wish I could make generic record helpers for arrays, though 😕 So, you use record helpers instead of: cProjCaptions[ptMain] ? Share this post Link to post
Lars Fosdal 1792 Posted November 12, 2019 Both. See updated post. Let me correct myself... With a record helper, the methods of the helper become part of the helped type. begin TProjectType.InitClass; // only for the second example var prj: TProjectType = ptMain; writeln(prj.Caption); writeln(ptMain.Caption) writeln(TProjectType.CaptionOf(ptMain)) end; all output the same string. Populating a combobox can be done like this for var prj := Low(TProjectType) to High(TProjectType) do ComboBox1.AddItem(prj.Caption, Pointer(Ord(prj))); and later, if you like such ugly hacks 😉 if ComboBox1.ItemIndex >= 0 then begin var selectedprj := TProjectType(Integer(ComboBox1.Items[ComboBox1.ItemIndex])); end; 1 Share this post Link to post
David Heffernan 2345 Posted November 12, 2019 49 minutes ago, Mike Torrettinni said: Are you talking about TCustomAttribute, like this answer: https://stackoverflow.com/questions/2134120/delphi-2010-rtti-explore-enumerations/2620883#2620883 Yeshttp://docwiki.embarcadero.com/RADStudio/en/Attributes_(RTTI) 1 Share this post Link to post
Lars Fosdal 1792 Posted November 12, 2019 For the latter example: begin TProjectType.InitHelper; var prj: TProjectType := ptExternalDev; Writeln(prj.Caption); end; will output External Dev Project Share this post Link to post
Lars Fosdal 1792 Posted November 12, 2019 @David Heffernan - Do you scan the rtti attributes once in a class init method/first use, or do you fetch the attributes at each use? Share this post Link to post
Mike Torrettinni 198 Posted November 12, 2019 17 minutes ago, Lars Fosdal said: For the latter example: begin TProjectType.InitHelper; var prj: TProjectType := ptExternalDev; Writeln(prj.Caption); end; will output External Dev Project Aha, I see. InitHelper initializes Caption, right? From const of names.., OR is Caption a function that return const_of_names[ptExternalDev]? Share this post Link to post
David Heffernan 2345 Posted November 12, 2019 22 minutes ago, Lars Fosdal said: @David Heffernan - Do you scan the rtti attributes once in a class init method/first use, or do you fetch the attributes at each use? I use a cache, a dictionary keyed on the type info pointer, iirc 1 Share this post Link to post
Lars Fosdal 1792 Posted November 12, 2019 14 minutes ago, Mike Torrettinni said: Aha, I see. InitHelper initializes Caption, right? From const of names.., OR is Caption a function that return const_of_names[ptExternalDev]? InitHelper initializes the class var arrays for Caption and DevTitle (for the second example using class vars). You only need to call this once for the application, so you could do f.x. it in the unit init section. For your second question, see the implementation of TProjectTypeHelper.Caption. Why the extra class function CaptionOf? A habit of mine, due to the rule of only one class helper for a type in scope at a time. Share this post Link to post
Lars Fosdal 1792 Posted November 12, 2019 19 minutes ago, David Heffernan said: I use a cache, a dictionary keyed on the type info pointer, iirc How do you associate the string attribute(s) with each enum value? Share this post Link to post
Bill Meyer 337 Posted November 12, 2019 A dictionary is nice, but in older versions -- I work a lot in D2007 -- then I make use of enums and const arrays. Defining them can be tedious, but maintenance is simple, and associated code is clean and easy. One thing to note on defining const arrays in the private section of a class: In D2007, I have found that defining consts in the class breaks the Ctrl-Shift-Up/Dn navigation between interface and implementation on members below the const declaration. I therefore put const declarations in the implementation section of the unit. Share this post Link to post
David Heffernan 2345 Posted November 12, 2019 (edited) 1 hour ago, Lars Fosdal said: How do you associate the string attribute(s) with each enum value? [Names('foo', 'bar')] TMyEnum = (foo, bar); It's kinda flaky of course because of the limitations on attribute constructor arguments being true constants. Later on I can write Enum.Name(someEnum) Edited November 12, 2019 by David Heffernan 1 Share this post Link to post
Mike Torrettinni 198 Posted November 12, 2019 Last question before I do some testing with new approaches: Would you reconsider your chosen approach if you have 100+ type of enums? If you have so much different data, that you need to repeat your implementation for 100+ times... would it make sense to still use your approach, or in such case you would (or have you) use something else? Share this post Link to post
Fr0sT.Brutal 900 Posted November 13, 2019 For such big numbers you can have all the data in any convenient form (CSV, JSON, XML) and write a tool that would generate a unit from this data. Share this post Link to post
Lars Fosdal 1792 Posted November 13, 2019 14 hours ago, Mike Torrettinni said: Last question before I do some testing with new approaches: Would you reconsider your chosen approach if you have 100+ type of enums? If you have so much different data, that you need to repeat your implementation for 100+ times... would it make sense to still use your approach, or in such case you would (or have you) use something else? There is no definitive answer to that as YMMV. We do literally have 100+ types of enums, and we implemented record helpers for each one, simply to ensure that all enums had the same capabilities. AsString, FromString, Name, and in some cases more texts, or biz.logic associated with the enum. We actually did the last push for this model this autumn, to get rid of the last couple of dozens of standalone TypeNameToStr functions. We also introduced app wide translations (Norwegian, Swedish, English) using Sisulizer. It turned out that using attributes for naming was a bit of a challenge, since you can't use a resourcestring as an argument to an attribute - go figure. resourcestring sName = 'Name'; type AttrNameAttribute = class(TCustomAttribute) constructor Create(const aName: String); end; type TSomeType = class private FaName: string; procedure SetaName(const Value: string); public [AttrName(sName)] // <-- [dcc32 Error] : E2026 Constant expression expected property aName: string read FaName write SetaName; end; We ended up setting names explicitly in code instead. 1 Share this post Link to post
Lars Fosdal 1792 Posted November 13, 2019 Come to think of it, it could have been nice to be able to enforce the helper implementations with an interface. or even better, be able to reuse an interface implementation for handling attribute naming. Also, this: /SendsLetterToCompilerSanta Share this post Link to post
Mike Torrettinni 198 Posted November 13, 2019 OK, it makes sense. I'm trying to find easiest solution to manage, but it's hard when in the middle of the progress I find out about different or better approach. Share this post Link to post
Mike Torrettinni 198 Posted November 13, 2019 3 hours ago, Fr0sT.Brutal said: For such big numbers you can have all the data in any convenient form (CSV, JSON, XML) and write a tool that would generate a unit from this data. Thank you, interesting idea, especially if they have very similar or even same structure. Share this post Link to post
Fr0sT.Brutal 900 Posted November 13, 2019 13 minutes ago, Mike Torrettinni said: OK, it makes sense. I'm trying to find easiest solution to manage, but it's hard when in the middle of the progress I find out about different or better approach. Well, you can define lots of overloaded functions like EnumDefValue, EnumCaption etc for now and implement them in any form. Later, if you find a structure you like more, you won't have to change the interface, only the internal implementation. Or, if you decide to switch to enum helpers, perform replace in files with regexp-s ( EnumDefValue\((.+?)\) => \1.DefValue ) 1 Share this post Link to post