[编程开发] 很多人都说 C# 语法怎么优雅,仅仅是因为 C# 的关键字多吗?

[复制链接]
aGDasdg 发表于 2023-10-3 19:28:34|来自:广东广州 | 显示全部楼层 |阅读模式
我发现很多说C#优雅的都是介绍与关键字相关的语法糖,如delegate、event、get、set、add、remove。虽然Java、Python没有提供这么多关键字,但完全可以实现
我把C#、Java和Python的最新版关键字做比较,C#有105个关键字,Java有50个,而Python只有35个,三者语法我都研究过,怎么我发现最优雅的反而是Python和Java,C#最差,甚至说别扭,看看大家评价
全部回复5 显示全部楼层
wjml223 发表于 2023-10-3 19:29:24|来自:广东广州 | 显示全部楼层
有些东西是语法糖,有些东西不是!不是说你能实现相同功能,别人就都变语法糖了。举个例子,in、out、ref 参数涉及 GC 对内部指针的支持。JVM 的 GC 不支持。那它的库 API 设计势必迁就这一点。例如原子性原语,Java API 只能人工将变量/字段擢升后用 AtomicInteger 的那种方法操纵数据,那还算优雅吗?.NET runtime 上的 C# 真的优雅太多了。当然理论上 Java 也可以加糖,让编译器帮你搞定。但现实就是不存在的,以后也不会存在的。还是这个特性,C++/CLI 是复用 const 关键字,引入托管引用 %,和增加类型 interior_ptr 来解决的。增加了 0 个关键字。 照题主的逻辑,最规整统一的 C++/CLI 反而因为关键字少最不优雅?什么道理,完全说不通嘛。
C# 就是奔着托管世界的 C/C++ 去的。它对底层的把握是极为精细的。Java、Python 都没这种能力。C# 的边界就是托管世界的工业化语言的开拓前缘。别跟我说什么 Scala 是 JVM 上的 C++,我完全不信。
一些地方变丑,主要原因还是因为底层的复杂性。来看看 C# 新语法——函数指针:
  1. //This method has a managed calling convention. This is the same as leaving the managed keyword off.
  2. delegate* managed<int, int>;
  3. // This method will be invoked using whatever the default unmanaged calling convention on the runtime
  4. // platform is. This is platform and architecture dependent and is determined by the CLR at runtime.
  5. delegate* unmanaged<int, int>;
  6. // This method will be invoked using the cdecl calling convention
  7. // Cdecl maps to System.Runtime.CompilerServices.CallConvCdecl
  8. delegate* unmanaged[Cdecl] <int, int>;
  9. // This method will be invoked using the stdcall calling convention, and suppresses GC transition
  10. // Stdcall maps to System.Runtime.CompilerServices.CallConvStdcall
  11. // SuppressGCTransition maps to System.Runtime.CompilerServices.CallConvSuppressGCTransition
  12. delegate* unmanaged[Stdcall, SuppressGCTransition] <int, int>;
复制代码
一个关键字也没增加,但就是丑了吧唧的。优不优雅和关键字的多少,我看就没有关系。
  1. public unsafe readonly struct EFI_CREATE_EVENT {
  2.     readonly delegate* unmanaged[Cdecl]<UINT32, EFI_TPL, EFI_EVENT_NOTIFY, void*, EFI_EVENT*, EFI_STATUS> value__;
  3.     EFI_CREATE_EVENT(delegate* unmanaged[Cdecl]<UINT32, EFI_TPL, EFI_EVENT_NOTIFY, void*, EFI_EVENT*, EFI_STATUS> value) => value__ = value;
  4.     public static implicit operator delegate* unmanaged[Cdecl]<UINT32, EFI_TPL, EFI_EVENT_NOTIFY, void*, EFI_EVENT*, EFI_STATUS>(EFI_CREATE_EVENT value) => value.value__;
  5.     public static implicit operator EFI_CREATE_EVENT(delegate* unmanaged[Cdecl]<UINT32, EFI_TPL, EFI_EVENT_NOTIFY, void*, EFI_EVENT*, EFI_STATUS> value) => new EFI_CREATE_EVENT(value);
  6.     /// <summary>
  7.     /// Creates an event.
  8.     /// </summary>
  9.     /// <param name="Type">The type of event to create and its mode and attributes.</param>
  10.     /// <param name="NotifyTpl">The task priority level of event notifications, if needed.</param>
  11.     /// <param name="NotifyFunction">The pointer to the event's notification function, if any.</param>
  12.     /// <param name="NotifyContext">
  13.     /// The pointer to the notification function's context; corresponds to parameter
  14.     /// Context in the notification function.
  15.     /// </param>
  16.     /// <param name="Event">
  17.     /// The pointer to the newly created event if the call succeeds; undefined
  18.     /// otherwise.
  19.     /// </param>
  20.     /// <returns>
  21.     /// <see cref="EFI_STATUS.EFI_SUCCESS">EFI_SUCCESS</see>
  22.     /// The event structure was created.<br/>
  23.     /// <see cref="EFI_STATUS.EFI_INVALID_PARAMETER">EFI_INVALID_PARAMETER</see>
  24.     /// One or more parameters are invalid.<br/>
  25.     /// <see cref="EFI_STATUS.EFI_OUT_OF_RESOURCES">EFI_OUT_OF_RESOURCES</see>
  26.     /// The event could not be allocated.<br/>
  27.     /// </returns>
  28.     public EFI_STATUS Invoke(
  29.         [IN] UINT32 Type,
  30.         [IN] EFI_TPL NotifyTpl,
  31.         [IN] EFI_EVENT_NOTIFY NotifyFunction,
  32.         [IN] void* NotifyContext,
  33.         [OUT] EFI_EVENT* Event) => value__(Type, NotifyTpl, NotifyFunction, NotifyContext, Event);
  34. }
复制代码
但加了新语法以后,不单为提高性能创造了可能性。语言的描述能力也变强了。你看用 C# 整系统开发,IoT 啥的,方便多了吧。C# 甚至能写 UEFI 应用程序(可裸机执行)。标准 Java 连自定义值类型类都没有。Python 就更别提了。你别说是 __slots__ ,那只是引用类型的结构布局。 Python 不靠疯狂封装 C/C++ 且带点元数据的脚本语言这一身份,它也是玩不通的。
再说说领域特定这块,Java 连运算符重载都没用,直接封死,要优雅都没有资格。C# 虽然毛病多,但有运算符重载和 Linq,至少还能玩玩。Python 倒是也能玩玩,但性能就不保证了。假如你要一个 Int32 执行溢出检查,但不抛异常,只是变成特殊值。那么 C# 中,你可以巧用 Nullable<T> 的类型推导规则,可以这么玩:
  1. // Copyright © 2020 LEI Hongfaan. Distributed under the MIT License.
  2. [Serializable]
  3. public readonly struct Int32CheckedNoThrow : IComparable, IFormattable, IConvertible
  4.     , IComparable<Int32CheckedNoThrow>, IEquatable<Int32CheckedNoThrow> {
  5.     private readonly Int32 Value;
  6.     public static readonly Int32CheckedNoThrow MaxValue = 2147483647;
  7.     public static readonly Int32CheckedNoThrow MinValue = -2147483648;
  8.     #region Comparisons
  9.     public static bool operator !=(Int32CheckedNoThrow first, Int32CheckedNoThrow second)
  10.         => !(first == second);
  11.     public static bool operator <(Int32CheckedNoThrow first, Int32CheckedNoThrow second)
  12.         => first.Value < second.Value;
  13.     public static bool operator <=(Int32CheckedNoThrow first, Int32CheckedNoThrow second)
  14.         => first.Value <= second.Value;
  15.     public static bool operator ==(Int32CheckedNoThrow first, Int32CheckedNoThrow second)
  16.         => first.Value == second.Value;
  17.     public static bool operator >(Int32CheckedNoThrow first, Int32CheckedNoThrow second)
  18.         => first.Value > second.Value;
  19.     public static bool operator >=(Int32CheckedNoThrow first, Int32CheckedNoThrow second)
  20.         => first.Value >= second.Value;
  21.     public int CompareTo(Int32CheckedNoThrow other) => Value.CompareTo(other.Value);
  22.     public int CompareTo(object? obj) {
  23.         {
  24.             if (obj is Int32CheckedNoThrow value) {
  25.                 return CompareTo(value);
  26.             }
  27.         }
  28.         {
  29.             if (obj is Int32 value) {
  30.                 return -value.CompareTo(this); // throw
  31.             }
  32.         }
  33.         return Value.CompareTo(obj); // throw
  34.     }
  35.     public override bool Equals(object? obj) {
  36.         {
  37.             if (obj is Int32CheckedNoThrow value) {
  38.                 return Equals(value);
  39.             }
  40.         }
  41.         {
  42.             if (obj is Int32 value) {
  43.                 return value.Equals(this); // throw
  44.             }
  45.         }
  46.         return Value.Equals(obj); // throw
  47.     }
  48.     public bool Equals(Int32CheckedNoThrow other) => Value.Equals(other.Value);
  49.     public override int GetHashCode() => Value.GetHashCode();
  50.     #endregion Comparisons
  51.     #region Constructors, Conversions
  52.     internal Int32CheckedNoThrow(int value) => Value = value;
  53.     public static implicit operator Int32(Int32CheckedNoThrow value) => value.Value;
  54.     public static implicit operator Int32?(Int32CheckedNoThrow? value) => value?.Value;
  55.     public static implicit operator Int32CheckedNoThrow(Int32 value) => new Int32CheckedNoThrow(value);
  56.     public static implicit operator Int32CheckedNoThrow(Int16 value) => new Int32CheckedNoThrow(value);
  57.     [CLSCompliant(false)]
  58.     public static implicit operator Int32CheckedNoThrow(UInt16 value) => new Int32CheckedNoThrow(value);
  59.     public static implicit operator Int32CheckedNoThrow(Byte value) => new Int32CheckedNoThrow(value);
  60.     [CLSCompliant(false)]
  61.     public static implicit operator Int32CheckedNoThrow(SByte value) => new Int32CheckedNoThrow(value);
  62.     [CLSCompliant(false)]
  63.     public static implicit operator Int32CheckedNoThrow(Char value) => new Int32CheckedNoThrow(value);
  64.     [MethodImpl(MethodImplOptions.AggressiveInlining)]
  65.     public static implicit operator Int32CheckedNoThrow?(Int32? value) => Unsafe.As<Int32?, Int32CheckedNoThrow?>(ref value);
  66.     public static Int32CheckedNoThrow Parse(string s, IFormatProvider? provider)
  67.         => Int32.Parse(s, provider);
  68.     public static Int32CheckedNoThrow Parse(string s, NumberStyles style, IFormatProvider? provider)
  69.         => Int32.Parse(s, style, provider);
  70.     public static Int32CheckedNoThrow Parse(string s) => Int32.Parse(s);
  71.     public static Int32CheckedNoThrow Parse(ReadOnlySpan<char> s, NumberStyles style = NumberStyles.Integer, IFormatProvider? provider = null)
  72.         => Int32.Parse(s, style, provider);
  73.     public static Int32CheckedNoThrow Parse(string s, NumberStyles style)
  74.         => Int32.Parse(s, style);
  75.     public static bool TryParse([NotNullWhen(true)] string? s, NumberStyles style, IFormatProvider? provider, out Int32CheckedNoThrow result) {
  76.         Unsafe.SkipInit(out result);
  77.         return Int32.TryParse(s, style, provider, out Unsafe.As<Int32CheckedNoThrow, Int32>(ref result));
  78.     }
  79.     public static bool TryParse(ReadOnlySpan<char> s, NumberStyles style, IFormatProvider? provider, out Int32CheckedNoThrow result) {
  80.         Unsafe.SkipInit(out result);
  81.         return Int32.TryParse(s, style, provider, out Unsafe.As<Int32CheckedNoThrow, Int32>(ref result));
  82.     }
  83.     public static bool TryParse(ReadOnlySpan<char> s, out Int32CheckedNoThrow result) {
  84.         Unsafe.SkipInit(out result);
  85.         return Int32.TryParse(s, out Unsafe.As<Int32CheckedNoThrow, Int32>(ref result));
  86.     }
  87.     public static bool TryParse([NotNullWhen(true)] string? s, out Int32CheckedNoThrow result) {
  88.         Unsafe.SkipInit(out result);
  89.         return Int32.TryParse(s, out Unsafe.As<Int32CheckedNoThrow, Int32>(ref result));
  90.     }
  91.     public bool TryFormat(Span<char> destination, out int charsWritten, ReadOnlySpan<char> format = default, IFormatProvider? provider = null)
  92.         => Value.TryFormat(destination, out charsWritten, format, provider);
  93.     public override string? ToString() => Value.ToString();
  94.     public string ToString(string? format, IFormatProvider? formatProvider)
  95.         => Value.ToString(format, formatProvider);
  96.     public string ToString(IFormatProvider? provider) => Value.ToString(provider);
  97.     public TypeCode GetTypeCode() => Value.GetTypeCode();
  98.     bool IConvertible.ToBoolean(IFormatProvider? provider) => Value.ToBoolean(provider);
  99.     byte IConvertible.ToByte(IFormatProvider? provider) => Value.ToByte(provider);
  100.     char IConvertible.ToChar(IFormatProvider? provider) => Value.ToChar(provider);
  101.     DateTime IConvertible.ToDateTime(IFormatProvider? provider) => Value.ToDateTime(provider);
  102.     decimal IConvertible.ToDecimal(IFormatProvider? provider) => Value.ToDecimal(provider);
  103.     Double IConvertible.ToDouble(IFormatProvider? provider) => Value.ToDouble(provider);
  104.     Int16 IConvertible.ToInt16(IFormatProvider? provider) => Value.ToInt16(provider);
  105.     Int32 IConvertible.ToInt32(IFormatProvider? provider) => Value.ToInt32(provider);
  106.     Int64 IConvertible.ToInt64(IFormatProvider? provider) => Value.ToInt64(provider);
  107.     sbyte IConvertible.ToSByte(IFormatProvider? provider) => Value.ToSByte(provider);
  108.     Single IConvertible.ToSingle(IFormatProvider? provider) => Value.ToSingle(provider);
  109.     string IConvertible.ToString(IFormatProvider? provider) => Value.ToString(provider);
  110.     object IConvertible.ToType(Type conversionType, IFormatProvider? provider) => Value.ToType(conversionType, provider);
  111.     UInt16 IConvertible.ToUInt16(IFormatProvider? provider) => Value.ToUInt16(provider);
  112.     UInt32 IConvertible.ToUInt32(IFormatProvider? provider) => Value.ToUInt32(provider);
  113.     UInt64 IConvertible.ToUInt64(IFormatProvider? provider) => Value.ToUInt64(provider);
  114.     #endregion Constructors, Conversions
  115.     #region Arithmetic Operations
  116.     public static Int32CheckedNoThrow? operator -(Int32CheckedNoThrow? value) {
  117.         if (value.HasValue) {
  118.             if (CheckedNoThrow.TryNegate(value.GetValueOrDefault(), out var result)) {
  119.                 return result;
  120.             }
  121.         }
  122.         return null;
  123.     }
  124.     public static Int32CheckedNoThrow? operator -(Int32CheckedNoThrow? first, Int32CheckedNoThrow? second) {
  125.         if (first.HasValue && second.HasValue) {
  126.             if (CheckedNoThrow.TrySubtract(first.GetValueOrDefault(), second.GetValueOrDefault(), out var result)) {
  127.                 return result;
  128.             }
  129.         }
  130.         return null;
  131.     }
  132.     public static Int32CheckedNoThrow? operator %(Int32CheckedNoThrow? first, Int32CheckedNoThrow? second) {
  133.         if (first.HasValue && second.HasValue) {
  134.             if (CheckedNoThrow.TryRemainder(first.GetValueOrDefault(), second.GetValueOrDefault(), out var result)) {
  135.                 return result;
  136.             }
  137.         }
  138.         return null;
  139.     }
  140.     public static Int32CheckedNoThrow operator &(Int32CheckedNoThrow first, Int32CheckedNoThrow second)
  141.         => first.Value & second.Value;
  142.     // This overload is optional. Define it here to avoid CS8620 false alerms.
  143.     public static Int32CheckedNoThrow? operator &(Int32CheckedNoThrow? first, Int32CheckedNoThrow? second) {
  144.         if (first.HasValue && second.HasValue) {
  145.             return first.GetValueOrDefault() & second.GetValueOrDefault();
  146.         }
  147.         return null;
  148.     }
  149.     public static Int32CheckedNoThrow? operator *(Int32CheckedNoThrow? first, Int32CheckedNoThrow? second) {
  150.         if (first.HasValue && second.HasValue) {
  151.             if (CheckedNoThrow.TryMultiply(first.GetValueOrDefault(), second.GetValueOrDefault(), out var result)) {
  152.                 return result;
  153.             }
  154.         }
  155.         return null;
  156.     }
  157.     public static Int32CheckedNoThrow? operator /(Int32CheckedNoThrow? first, Int32CheckedNoThrow? second) {
  158.         if (first.HasValue && second.HasValue) {
  159.             if (CheckedNoThrow.TryDivide(first.GetValueOrDefault(), second.GetValueOrDefault(), out var result)) {
  160.                 return result;
  161.             }
  162.         }
  163.         return null;
  164.     }
  165.     public static Int32CheckedNoThrow operator ^(Int32CheckedNoThrow first, Int32CheckedNoThrow second) {
  166.         return first.Value ^ second.Value;
  167.     }
  168.     // This overload is optional. Define it here to avoid CS8620 false alerms.
  169.     public static Int32CheckedNoThrow? operator ^(Int32CheckedNoThrow? first, Int32CheckedNoThrow? second) {
  170.         if (first.HasValue && second.HasValue) {
  171.             return first.GetValueOrDefault() ^ second.GetValueOrDefault();
  172.         }
  173.         return null;
  174.     }
  175.     public static Int32CheckedNoThrow operator |(Int32CheckedNoThrow first, Int32CheckedNoThrow second) {
  176.         return first.Value | second.Value;
  177.     }
  178.     // This overload is optional. Define it here to avoid CS8620 false alerms.
  179.     public static Int32CheckedNoThrow? operator |(Int32CheckedNoThrow? first, Int32CheckedNoThrow? second) {
  180.         if (first.HasValue && second.HasValue) {
  181.             return first.GetValueOrDefault() | second.GetValueOrDefault();
  182.         }
  183.         return null;
  184.     }
  185.     public static Int32CheckedNoThrow operator ~(Int32CheckedNoThrow value) {
  186.         return ~value.Value;
  187.     }
  188.     public static Int32CheckedNoThrow operator +(Int32CheckedNoThrow value) {
  189.         return value;
  190.     }
  191.     public static Int32CheckedNoThrow? operator +(Int32CheckedNoThrow? value) {
  192.         return value;
  193.     }
  194.     public static Int32CheckedNoThrow? operator +(Int32CheckedNoThrow? first, Int32CheckedNoThrow? second) {
  195.         if (first.HasValue && second.HasValue) {
  196.             if (CheckedNoThrow.TryAdd(first.GetValueOrDefault(), second.GetValueOrDefault(), out var result)) {
  197.                 return result;
  198.             }
  199.         }
  200.         return null;
  201.     }
  202.     public static Int32CheckedNoThrow operator <<(Int32CheckedNoThrow value, int count)
  203.         => value.Value << count;
  204.     public static Int32CheckedNoThrow? operator <<(Int32CheckedNoThrow value, int? count)
  205.         => value.Value << count;
  206.     // This overload is optional. Define it here to avoid CS8620 false alerms.
  207.     // This overload also suppresses CS8629.
  208.     public static Int32CheckedNoThrow? operator <<(Int32CheckedNoThrow? value, int? count) {
  209.         if (value.HasValue && count.HasValue) {
  210.             return value.GetValueOrDefault() << count.GetValueOrDefault();
  211.         }
  212.         return null;
  213.     }
  214.     public static Int32CheckedNoThrow operator >>(Int32CheckedNoThrow value, int count)
  215.         => value.Value >> count;
  216.     public static Int32CheckedNoThrow? operator >>(Int32CheckedNoThrow value, int? count)
  217.         => value.Value >> count;
  218.     // This overload is optional. Define it here to avoid CS8620 false alerms.
  219.     // This overload also suppresses CS8629.
  220.     public static Int32CheckedNoThrow? operator >>(Int32CheckedNoThrow? value, int? count) {
  221.         if (value.HasValue && count.HasValue) {
  222.             return value.GetValueOrDefault() >> count.GetValueOrDefault();
  223.         }
  224.         return null;
  225.     }
  226.     #endregion Arithmetic Operations
  227. }
复制代码
用起来就和语言自带的 int 几乎一致。Java 遇到类似问题可就傻眼了。你看看 Java 操作无符号整数就知道了,遑论其他。
再说一个,lambda 函数的类型推导也是。题主是要把,只能到处插入类型转换的 Java,也称为语法上优雅么?那我还是别优雅了。
tdq6554 发表于 2023-10-3 19:30:14|来自:广东广州 | 显示全部楼层
嗯,C++标准委员会不愿意增加关键字的原因终于找到了,原来关键字最少的语言最优雅……
感谢提问者破解了这一跨世纪谜题……


我认为和一个研究语法去数关键字个数的人讨论这种问题和对牛弹琴没啥区别。

毕竟,草泥马语言关键字比这些都少,而Brainfuck语言甚至连关键字都没有……
更重要的是,他们竟然都能实现同样的功能……
dddddno1 发表于 2023-10-3 19:30:31|来自:广东广州 | 显示全部楼层
关键字与否其实是一个前端parser的取舍问题,Python充分利用动态特性将这些功能实现为magic method(比如__init__),这样就不用占额外的关键字。还有的关键字,在某些语言中是用运算符表示的,你显然也没有统计进去。光看数量意义不大。
Java的问题是语法特性有点少,而且比较理想化,不贴近应用(比如不支持多返回值,不支持函数引用,不支持属性等),导致这些特性往往需要依赖反射自己实现出来,反而导致实际使用中有很多超出语言范围的隐藏规则(比如getter/setter的命名)自然就不那么优雅了。相比来说,C#比较务实,实际使用时需要的特性都往里加,其中大部分是很方便的,不过偶尔也有很别扭的地方(比如IDisposable,泛型接口和非泛型接口)。许多特性也是后来的Java版本追赶的目标。
wuchao 发表于 2023-10-3 19:31:04|来自:广东广州 | 显示全部楼层
优雅这个词,在我看来,有点类似于情怀、工匠精神,原意很好,但是现实中已经基本上被毁了。现在我一点也不想提它。
关键字这个指标没多大现实意义,也不适合作为评价标准。C++有70多个关键字,难道说明C++介于C#和Java之间吗?
对于语法问题我没什么意见。不过有一点我很确信:C#的标准库——严格来说应该是.Net BCL,总体来说设计是非常清晰且合理的,比Java和Python的标准库都要好。Java一些早期设计简直就是OO的反面教材,为了向下兼容还难以修改。Python除了核心库外,很多内置库的设计不够统一,有些还比较随意。当然,BCL部分面向特定领域的API也有不合理的地方,Java和Python某些库也有特别出色之处,但就整体而言,还是BCL的设计更有美感。
我是色狼 发表于 2023-10-3 19:31:52|来自:广东广州 | 显示全部楼层
我们大概对优雅的理解有些不同

我个人认为,所谓的优雅,是利用较少的代码量实现同样的功能,同时保持足够的可读性。

举个例子来说,函数的功能是遍历一个容器,把里面符合条件的对象复制一份放入新的容器中,最后返回这个新的容器。那么这个功能用不同语言写出来大概是什么样子呢

C语言因为没有对象,所以大概会写成这个样子
  1. collection* selectObj(collection* orignal){
  2.     if(!original || !original->size) return NULL;
  3.     collection* newcol = (collection*)malloc(sizeof(collection));
  4.     newcol->size = 0;
  5.     newcol->head = (object*)malloc(sizeof(object));
  6.     // a lot of code to init new object or call an external init function
  7.     object* cur = original->head;
  8.     while(cur != NULL){
  9.         if(checkObj(cur)){
  10.              // do copy works and insert
  11.         }   
  12.         cur = cur->next
  13.         //some other works
  14.     }
  15.     return newcol;
  16. }
复制代码
C++11大概可以写成这种
  1. collection<object*> selectObj(const collection<object*>& original)   {
  2.     collection<object*> newcol;
  3.     for(auto ptr : original){
  4.          if(check(ptr)){
  5.               newcol.emplace_back(new object(*ptr));
  6.          }
  7.     }
  8.     return newcol;
  9. }
复制代码
C#则是这种样子
  1. var newcol = from MyObj in original where check(MyObj) select MyObj;
复制代码
可以看到相比于同门的C和C++(JAVA长的也几乎一样),c#在一行搞定相同功能的同时,保持了代码良好的可读性和可维护性,所以会显得更加的优雅。

快速回帖

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则