C# enumerate advanced tactics

Posted by MerMer on Tue, 08 Mar 2022 20:31:23 +0100

This article is transferred from: https://www.cnblogs.com/willick/p/csharp-enum-superior-tactics.html , please click the link to view the original text and respect the copyright of the landlord.

At the beginning of the article, I'll give you an interview question:

When designing the database of a small project (assuming MySQL is used), if you add a field (Roles) to the User table to store the User's role, what type will you set for this field? Tip: consider that Roles need to be represented by enumeration during back-end development, and a User may have multiple Roles.

The first answer that comes to your mind may be: varchar type, which uses separators to store multiple roles, such as 1|2|3 or 1,2,3 to indicate that users have multiple roles. Of course, if the number of roles may exceed a single digit, considering the convenience of database query (for example, using INSTR or POSITION to judge whether the user contains a role), the value of the role should start from the number 10 at least. The plan is feasible, but it is not too simple. Is there a better plan? The better answer should be integer (int, bigint, etc.), which has the advantage that it is more convenient to write SQL query conditions, and its performance and space are better than varchar. But after all, integer is just a number. How to represent multiple roles? At this time, when you think of binary operation, you should have the answer in your heart. And keep the answers in your mind, and then after reading this article, you may have unexpected gains, because you may encounter a series of problems in practical application. In order to better illustrate the following problems, let's first review the basic knowledge of enumeration.

1. Enumeration basis

The function of enumeration type is to limit its variables to take values from limited options. These options (members of enumeration type) correspond to a number respectively. The number starts from 0 by default and increases accordingly. For example:

public enum Days
{
    Sunday, Monday, Tuesday, // ...
}

Where the value of Sunday is 0, Monday is 1, and so on. In order to see the value represented by each member at a glance, it is generally recommended to write out the member value explicitly and do not omit:

public enum Days
{
    Sunday = 0, Monday = 1, Tuesday = 2, // ...
}

The type of C# enumeration members is int by default. Enumeration members can be declared as other types through inheritance, such as:

public enum Days : byte
{
    Monday = 1,
    Tuesday = 2,
    Wednesday = 3,
    Thursday = 4,
    Friday = 5,
    Saturday = 6,
    Sunday = 7
}

The enumeration type must be one of byte, sbyte, short, ushort, int, uint, long and ulong, and cannot be any other type. The following are the common uses of several enumerations (take the Days enumeration above as an example):

// Enumeration to string
string foo = Days.Saturday.ToString(); // "Saturday"
string foo = Enum.GetName(typeof(Days), 6); // "Saturday"
// String to enumeration
Enum.TryParse("Tuesday", out Days bar); // true, bar = Days.Tuesday
(Days)Enum.Parse(typeof(Days), "Tuesday"); // Days.Tuesday

// Enumeration to number
byte foo = (byte)Days.Monday; // 1
// Digital to enumeration
Days foo = (Days)2; // Days.Tuesday

// Gets the numeric type to which the enumeration belongs
Type foo = Enum.GetUnderlyingType(typeof(Days))); // System.Byte

// Get all enumeration members
Array foo = Enum.GetValues(typeof(MyEnum);
// Gets the field names of all enumeration members
string[] foo = Enum.GetNames(typeof(Days));

In addition, it is worth noting that enumeration may get unexpected values (values have no corresponding members). For example:

Days d = (Days)21; // No error will be reported
Enum.IsDefined(typeof(Days), d); // false

Even if the enumeration does not have a member with a value of 0, its default value is always 0.

var z = default(Days); // 0

Enumeration can add useful auxiliary information for members through features such as Description and Display, such as:

public enum ApiStatus
{
    [Description("success")]
    OK = 0,
    [Description("Resource not found")]
    NotFound = 2,
    [Description("access denied")]
    AccessDenied = 3
}

static class EnumExtensions
{
    public static string GetDescription(this Enum val)
    {
        var field = val.GetType().GetField(val.ToString());
        var customAttribute = Attribute.GetCustomAttribute(field, typeof(DescriptionAttribute));
        if (customAttribute == null) { return val.ToString(); }
        else { return ((DescriptionAttribute)customAttribute).Description; }
    }
}

static void Main(string[] args)
{
    Console.WriteLine(ApiStatus.Ok.GetDescription()); // "Success"
}

I think the above has covered most of the enumeration knowledge we use everyday. Let's go back to the problem of user role storage at the beginning of the article.

2. User role storage problem

We first define an enumeration type to represent two user roles:

public enum Roles
{
    Admin = 1,
    Member = 2
}

In this way, if a User has both Admin and Member Roles, the Roles field of the User table should be saved as 3. That's the problem. At this time, how to write the SQL of all users with Admin role? For basic programmers, this problem is very simple. Just use the bitwise operator logical and ('&') to query.

SELECT * FROM `User` WHERE `Roles` & 1 = 1;

Similarly, for querying users with both roles, the SQL statement should be written as follows:

SELECT * FROM `User` WHERE `Roles` & 3 = 3;

For this SQL statement, C # is used to implement the query. It is as follows (for simplicity, Dapper is used here):

public class User
{
    public int Id { get; set; }
    public Roles Roles { get; set; }
}

connection.Query<User>(
    "SELECT * FROM `User` WHERE `Roles` & @roles = @roles;",
    new { roles = Roles.Admin | Roles.Member });

Correspondingly, in C#, to judge whether a user has a role, you can judge as follows:

// Mode 1
if ((user.Roles & Roles.Admin) == Roles.Admin)
{
    // Do what administrators can do
}

// Mode 2
if (user.Roles.HasFlag(Roles.Admin))
{
    // Do what administrators can do
}

Similarly, in C #, you can perform any bit logical operation on the enumeration, such as removing the role from an enumeration variable:

var foo = Roles.Admin | Roles.Member;
var bar = foo & ~Roles.Admin;

This solves the problem of using integer to store multiple roles mentioned earlier in the article. It is feasible in both database and C# language, and it is also very convenient and flexible.

3. Enumerated Flags

Next, we provide a method to query users through roles and demonstrate how to call it, as follows:

public IEnumerable<User> GetUsersInRoles(Roles roles)
{
    _logger.LogDebug(roles.ToString());
    _connection.Query<User>(
        "SELECT * FROM `User` WHERE `Roles` & @roles = @roles;",
        new { roles });
}

// call
_repository.GetUsersInRoles(Roles.Admin | Roles.Member);

Roles. Admin | Roles. The value of member is 3. Since a field with a value of 3 is not defined in the roles enumeration type, the roles parameter in the method displays 3. This information is very unfriendly to our debugging or printing logs. In the method, we don't know what this 3 represents. To solve this problem, C# enumeration has a very useful feature: FlagsAtrribute.

[Flags]
public enum Roles
{
    Admin = 1,
    Member = 2
}

After adding the Flags feature, when we debug the getusersinroles (roles) method again, the value of the roles parameter will be displayed as Admin|Member. In short, the difference between adding Flags and not adding Flags is:

var roles = Roles.Admin | Roles.Member;
Console.WriteLing(roles.ToString()); // "3", no Flags feature
Console.WriteLing(roles.ToString()); // "Admin, Member", with Flags feature

Adding Flags to enumerations should be regarded as a best practice of C# programming, and Flags should be added as much as possible when defining enumerations.

4. Resolve enumeration value conflict: power of 2

Now, what will happen if you add a role to the enumeration? According to the rule of increasing the number value, the value of Manager should be set to 3.

[Flags]
public enum Roles
{
    Admin = 1,
    Member = 2,
    Manager = 3
}

Can you set the value of Manager to 3? Obviously not, because the value of Admin and Member for bit or logical operation (i.e. Admin | Member) is also 3, indicating that they have both roles, which conflicts with the Manager. How to set the value to avoid conflict? Since the binary logical operation "or" will conflict with the Member value, use the law of logical operation or to solve it. We know that the logic of "or" operation is that as long as there is a 1 on both sides, the result will be 1. For example, the results of 1|1 and 1|0 are all 1, and only in the case of 0|0, the result will be 0. Then we need to avoid any two values appearing 1 in the same position. According to the characteristics of binary full 2 into 1, as long as the enumerated values are guaranteed to be the power of 2. For example:

1:  00000001
2:  00000010
4:  00000100
8:  00001000

If you add 16, 32, 64... Later, no matter how the values are added, they will not conflict with any value of the member. In this way, the problem is solved, so we want to define the value of the Roles enumeration as follows:

[Flags]
public enum Roles
{
    Admin = 1,
    Member = 2,
    Manager = 4,
    Operator = 8
}

However, when defining the value, you should calculate it in your mind. If you want to be lazy, you can use the following "displacement" method to define it:

[Flags]
public enum Roles
{
    Admin    = 1 << 0,
    Member   = 1 << 1,
    Manager  = 1 << 2,
    Operator = 1 << 3
}

Just keep increasing the value downward. It has a good reading experience and is not easy to make mistakes. The two methods are equivalent. The calculation of constant displacement is carried out at the time of compilation, so there will be no additional overhead.

5. Summary

In this paper, a small interview question leads to a series of thoughts on enumeration. In small systems, it is common to store user roles directly in the user table. At this time, setting the role field to integer (such as int) is a better design scheme. But at the same time, some best practices should also be considered, such as using the Flags feature to help better debugging and log output. We should also consider various potential problems in actual development, such as the conflict between multiple enumeration values or ('|') operations and member values.

Reprint statement:

Author: exquisite code farmer

source: http://cnblogs.com/willick

Contact: Liam wang@live.com

The copyright of this article belongs to the author and the blog park. Reprint is welcome, but this statement must be retained without the consent of the author, and the original text connection must be given in an obvious position on the article page, otherwise the right to investigate legal responsibility is reserved. If you have any questions or suggestions, please give me more advice. Thank you very much.