首页 > 编程语言 >《NET CLR via C#》---第五章(基元类型,引用类型和值类型,对象相等性和同一性,对象哈希码)

《NET CLR via C#》---第五章(基元类型,引用类型和值类型,对象相等性和同一性,对象哈希码)

时间:2024-08-12 20:19:27浏览次数:12  
标签:via C# System Equals 对象 实例 类型 装箱

基元类型

编译器直接支持的数据类型称为基元类型(primitvie type),基元类型直接映射到Framework类库(FCL)中存在的类型。例如,C#的int直接映射到System.Int32类型。因此,以下4行代码都能正确编译,并生成完全相同的IL:

int a1 = 0;                             // 最方便的语法
System.Int32 a2 = 0;                    // 方便的语法
int a3 = new int();                     // 不方便的语法
System.Int32 a4 = new System.Int32();   // 最不方便的语法

除此之外,只要是符合公共语言规范(CLS)的类型,其他语言都提供了类型的基元类型(不符合CLS的类型语言就不一定支持了)。下面列一下C#基元类型与对应的FCL类型。

C#基元类型 FCL类型 符合CLS 说明
sbyte System.SBype 有符号8位值
byte System.Byte 无符号8位值
short System.Int16 有符号16位值
ushort System.UInt16 无符号16位值
int System.Int32 有符号32位值
uint System.UInt32 无符号32位值
long System.Int64 有符号64位值
ulong System.UInt64 无符号64位值
char System.Char 16位Unicode字符
float System.Single IEEE 32位浮点值
double System.Double IEEE 64位浮点值
bool System.Boolean true/false值
decimal System.Decimal 128位高精度浮点值,常用于不容许舍入误差的金融计算
string System.String 字符数组
object System.Object 所有类型的基类型
dynamic System.Object 对于CLR,dynamic和object完全一致。但C#编译器允许使用简单的语法让dynamic变量参与动态调度

从另一个角度讲,可以认为C#编译器自动为所有源代码文件都添加了以下using指令:
using sbyte = System.SByte;
using byte = System.Byte;
............................................

C#语言规范称:“从风格上说,最好使用关键字,而不是完整的系统类型名称”。作者不同意该观点,我不太赞同作者的观点,就不把作者的想法收录进去了。

基元类型的转型

在许多编程语言中,以下代码都能正确编译并运行:

int i = 5;	// 32位值
long l = i;	// 隐式转型为64位值

第一次看,可能会感觉比较奇怪,int和long明明是不同类型,且没有任何派生关系,但为什么C#编译器可以编译上述代码,且运行起来也没有问题?

原因是C#编译器非常熟悉基元类型,会在编译代码时应用自己的特殊规则。也就是说,编译器能识别常见的编程模式,并生成必要的IL。具体来说,C#编译器支持与类型转换、字面值(例如123.toString() 123就是字面值,或者"abc".Length,"abc"是字面值)以及操作符有关的模式。

不过,只有在转换“安全”的时候,C#才允许隐式转型。所谓“安全”,就是指不会发生数据丢失的情况,比如从int32(int)转换为int64(long)。但如果可能不安全,C#就要求显式转型(对于数值类型,“不安全”意味着转型后可能丢失精度或数量级)。例如int转换为Byte要求显式转型,因为大的Int32数字可能丢失精度。例如:

int i = 300;
float f = i;		// 从int隐式转型为float
byte b = (byte)i;	// 从inte显式转型为byte

注意,不同编译器可能生成不同的代码来处理这些转型。C#总是对结果进行截断(或者说是取模),而不进行向上取整。例如上例中,b的值会是44(300%256=44)。

除了转型,基本类型还能写成字母值(literal)。字面值可以被看成类型本身的实例,所以像下面代码可以执行:

Console.WriteLine(123.ToString());	// 输出:123

除此之外,如果表达式由字面值构成,编译器在编译时就能完成表达式求值,从而增强应用程序性能:

int x = 100 + 20 + 3;	// 生成的代码将x设为123

checked和unchecked基元类型操作

对基元类型执行的许多算术运算都可能造成溢出:

Byte b = 100;
b = (Byte)(b + 100);
------------
在执行上述运算时,第一步会把所有操作数都扩大为32位值(或者64位值,如果任何操作数需要超过32位来表示的话)。所以b和200首先转为32位值,然后加到一起。结果是一个32位值,但在存回变量b前必须转型为Byte。C#无法隐式做这个转型,所以需要程序员强制转型。

C#允许程序员自己决定如何处理溢出。溢出检查默认关闭。也就是说,编译器生成IL代码时,将自动使用加减乘除以及转换指令的无溢出检查版本(结果是代码能更快运行——但需要我们保证不发生溢出,或者我们能预见到)。

让C#编译器控制溢出的一个办法是使用/checked+编译器开关。该开关指示编译器在生成代码时,使用加减乘除和转换指令的溢出检查版本。这样生成的代码在执行时会稍慢一些,因为CLR会检查这些运算,判断是否发生溢出。如果发生溢出,CLR会抛出OverflowException异常。

除了全局性的打开或关闭溢出检查,程序员还可在代码的特定区域控制溢出检查。C#通过checked和unchecked操作符来提供这种灵活性。例如:

uint i = unchecked((uint)(-1));	// 能够执行,输出:4294967295

下面例子使用了checked操作符:

byte b = 100;
b += checked((byte)(b + 200));

b和200首先转换为32位值,加到一起,结果是300.然后做显式转换,300明显大于byte的最大值255,造成OverflowException异常。会抛出报错:未经处理的异常: System.OverflowException: 算术运算导致溢出。如果将byte转型放到checked操作符外部则不会抛出异常。

byte b = 100;
b += (byte)checked((b + 200));

除了checked和unchecked操作符,C#还支持checked和unchecked语句,它们造成一个块中的所有表达式都进行或不进行溢出检查,例如:

checked
{
    byte b = 100;
    b += 200;
}
--------------------
会抛出报错:未经处理的异常:  System.OverflowException: 算术运算导致溢出

作者对程序员判断是否溢出,做出了几点总结,我摘抄如下:

  1. 尽量使用有符号数值类型(比如int和long)而不是无符号数值类型(比如uint和ulong)。这允许编译器检测更多的上溢/下溢错误。除此之外,类库多个部分(比如Array和String的Length熟悉)被硬编码返回有符号的值。这样在代码中四处移动这些值时,需要进行的强制类型转换就少了。较少的强类型转换使代码更整洁,更容易维护。除此之外,无符号数值类型不符合CLS。
  2. 写代码时,如果代码可能发生你不希望的溢出,就把这些代码放到checked块中。同时捕捉OverflowException,得体地从错误中恢复。
  3. 写代码时,将允许发生溢出的代码显式放到unchecked块中,比如在计算校验和时。
  4. 对于没有使用checked或unchecked的任何代码,都假定你希望在发生溢出时抛出一个异常,比如在输入已知的前提下计算一些东西(比如质数),此时溢出应被计为bug。

开发应用程序时,打开编译器的/checked+开关进行调试性生成。这样系统会对没有显式标记checked或unchecked的代码进行溢出检测,所以应用程序运行起来会慢一些。此时发生异常,就能轻松检测到,并能及时修复代码的bug。但是,正式发布时,应该使用编译器的/checked-开关,确保代码能更快运行。

要在Microsoft Visual Studio中更改Checked设置,请打开项目的属性页,点击“生成”标签,单击“高级”,再勾选“检查运算上溢/下溢”
image

引用类型和值类型

CLR支持两种类型:引用类型和值类型。引用类型总是从托管堆分配,c#的new操作符返回对象内存地址——即指向对象数据的内存地址。使用引用类型必须留意性能问题。

  1. 内存必须从托管堆分配
  2. 堆上分配的每个对象都有一些额外成员,这些成员必须初始化
  3. 对象中的其他字节(为字段而设)总是设为零
  4. 从托管堆分配对象时,可能强制执行一次垃圾回收

如果所有类型都是引用类型,应用程序的性能将显著下降!设想下每次使用int值时都进行一次内存分配,性能会受到多大的影响。CLR提供了名为“值类型”的轻量级类型。

  1. 值类型的实例一般在线程栈上分配。
  2. 在代表值类型实例的变量中不包含指向实例的指针。相反,变量中包含了实例本身的字段。
  3. 由于变量已包含了实例的字段,所以操作实例中的字段不需要提领指针(提领(Dereference)指针意味着通过指针获取它指向的具体数据,如果你有一个指针p指向一个整数变量x,那么通过*p可以访问x的值)。
  4. 值类型的实例不受垃圾回收器的控制。
  5. 值类型的使用缓解了托管堆的压力,并减少了应用程序生存期内的垃圾回收次数。

下面提供更细的引用类型与值类型的区别:

  1. 任何称为“类”的类型都是引用类型,相反,所有值类型都称为结构或枚举。
  2. 所有结构都是抽象类型System.ValueType的直接派生类,System.ValueType本身又直接从System.Object派生。
  3. 根据定义,所有值类型都必须从System.ValueType派生。所有枚举都从System.Enum抽象类型派生,后者又从System.ValueType派生。
  4. 不能在定义值类型时,为它指定基类,但值类型可以实现一个或者多个接口。
  5. 所有值类型都隐式密封,目的是防止将值类型用作其他引用类型或值类型的基类型。例如,无法将Boolean,Char,Int32等作为基类来定义任何新类型。

image
设计自己类型时,要仔细考虑类型是否应该定义成值类型而不是引用类型。值类型有时能提供更好的性能。具体来说,除非满足以下全部条件,否则不应将类型声明为值类型。

  • 类型具有基元类型的行为。也就是说,是十分简单的类型,没有成员会修改类型的任何实例字段。如果类型没有提供会更改其字段的成员,就说该类型是不可变(immutable)类型。事实上,对于许多值类型,我们都建议将全部字段标记为readonly。
  • 类型不需要从任何其他类型继承。
  • 类型也不能派生出其他任何类型。
  • 类型的实例较小(16字节或更小);实例的实例较大(大于16字节),但不作为方法实参传递,也不从方法返回,因为作为实参传递时,是以传值方式进行传递,会对值类型中字段进行复制。

值类型的主要优势是不作为对象在托管堆上分配。当然,与引用类型相比,值类型也存在自身的一些局限:

  • 值类型有2种表示形式:已装箱和未装箱;引用类型总是处于已装箱形式。
  • 值类型从System.ValueType派生。该类型提供了与System.Object相同的方法。但System.ValueType重写了Equals方法(反射的方式),能在两个对象的字段值完全匹配的前提下返回true。此外,System.ValueType重写了GetHashCode方法。生成哈希码值时,这个重写方法所用的算法会将对象的实例字段种的值考虑在内。由于这个默认实现存在性能问题,所以定义自己的值类型应重写Equals和GetHashCode方法,并提供它们的显式实现。
  • 由于不能将值类型作为基类型来定义新的值类型或者新的引用类型,所以不应在值类型种引入任何新的虚方法。所有方法都不能是抽象的,所有方法都隐式密封(不可重写)。
  • 引用类型的变量包含堆中对象的地址。引用类型的变量创建时默认初始化为null,表明当前不指向有效对象。视图使用null引用类型变量会抛出NullReferenceException异常;相反,值类型的变量总是包含其基础类型的一个值,而且值类型的所有成员都初始化为0.值类型变量不是指针,访问值类型不可能抛出NullReferenceException异常。
  • 将值类型变量赋给另一个值类型变量,会执行逐字段的复制。将引用类型的变量赋给另一个引用类型的变量只复制内存地址。
  • 两个或多个引用类型变量能引用堆种同一个对象,所以对一个变量执行的操作可能影响到另一个变量引用的对象。相反,值类型变量自成一体,对值类型变量执行的操作不可能影响到另一个值类型变量。
  • 由于未装箱的值类型不在堆上分配,一旦定义了该类型的一个实例的方法不再活动,为它分配的内存就会被释放,而不是等着垃圾回收。

值类型的装箱和拆箱

值类型比引用类型“轻”,原因是它们不作为对象在托管堆中分配,不被垃圾回收,也不通过指针进行引用。将值类型转换成引用类型要使用装箱机制,C#编译器自动生成对值类型实例进行装箱所需的IL代码,下面列举装箱时内部发生的事情:

  1. 在托管堆中分配内存。分配的内存量是值类型各字段所需的内存量,还要加上托管堆所有对象都有的两个额外成员(类型对象指针和同步块索引)所需的内存量。
  2. 值类型的字段复制到新分配的堆内存。
  3. 返回对象地址。现在该地址是对象引用;值类型成了引用类型。

了解了装箱,再了解一下拆箱:

  1. 首先获取引用,取得装箱对象中各个字段的地址。这一步就称为拆箱。
  2. 然后将装箱对象中的所有字段复制到线程栈上的值类型变量中。

拆箱不是直接将装箱的过程倒过来。拆箱的代价比装箱低得多。拆箱其实就是获取指针的过程,该指针指向包含在一个对象中的原始值类型(数据字段)。其实,指针指向的是已装箱实例中的未装箱部分。所以和装箱不同,拆箱不要求在内存中复制任何字节。此外,在拆箱完成后,会立即进行一次字段复制。

已装箱值类型实例在拆箱时,内部发生下面这些事情。

  1. 如果包含“对已装箱值类型实例的引用”的变量为null,抛出NullReferenceException异常。
  2. 如果引用的对象不爽所需值类型的已装箱实例,抛出InvalidCastExcetion异常。

第二条则意味着,在以下代码中:

public class Program
{
    static void Main(string[] args)
    {
        int a = 10;
        long b1 = a;

        object o = a;
        long b2 = (long)o;
    }
}

b2的转换会报错,抛出System.InvalidCastException异常。

除此之外还有一点要注意的是,如果你想更改已装箱实例的值,你必须将它拆箱后进行修改再重新进行装箱。十分影响程序的性能,写法也很繁琐,例如:

public class Program
{
    static void Main(string[] args)
    {
        int a = 10;
        object o = a;
        a = (int)o;
        a = 15;
        o = a;

        Console.WriteLine(o);
    }
}

有的语言(比如C++/CLI)允许在不复制字段的前提下对已装箱的值类型进行拆箱。拆箱返回已装修对象中的未装箱部分的地址(忽略对象“类型对象指针”和“同步块索引”这2各额外的成员)。接着可利用这个指针来操纵未装箱实例的字段(这些字段恰好在堆上已装箱对象中)。所以,如果装箱/拆箱比较频繁的部分,交由C++来处理也许会更好,因为这起码避免了为了修改字段值导致的拆箱。

仔细研究一下FCL,会发现许多方法都针对不同的值类型参数进行了重载。例如,System.Console.WriteLine有:

public static void WriteLine(Boolean);
public static void WriteLine(Char);
public static void WriteLine(Int32);
....

大多数方法进行重载唯一目的就是减少常用值类型的装箱次数。通过这些例子,我们很容易判断出一个值类型的实例在什么时候需要装箱:

  1. 要获取对值类型实例的引用,实例必须装箱。
  2. 将值类型实例传给需要需要获取引用类型的方法,也会发生这种情况。

未装箱值类型比引用类型更“轻”,则要归功于两个原因:

  1. 不在托管堆上分配
  2. 没有堆上每个对象都有的额外成员:“类型对象指针”和“同步块索引”

由于未装箱值类型没有同步块索引,所以不能使用System.Threading.Monitor类型的方法(或者C# lock语句)让多个线程同步对实例的访问。

虽然未装箱值类型没有类型对象指针,但仍可调用由类型继承或重写的虚方法(比如Equals,GetHashCode或者ToString)。如果值类型重写了其中任何虚方法,那么CLR可以以非虚地调用该方法,因为值类型隐式密封,不可能由类型从它们派生,而且调用虚方法的值类型没有装箱。例如ToString()方法:

pulic struct MyStruct1
{
	public override void ToString(){}
}

public struct MyStruct2{}

....
MyStruct1 struct1 = new MyStruct1();
MyStruct2 struct2 = new MyStruct2();
struct1.ToString();	// 不用装箱
struct2.ToString();	// 要装箱

之所以不需要装箱,是因为重写以后,在编译阶段,JIT编译器能够直接解析出方法的内存地址或偏移量,从而在生成的机器代码中嵌入对该方法的直接调用。

然而,如果在值类型中重写了一个虚方法,并且在重写的方法中需要调用基类版本的该方法,那么当调用基类的方法时,值类型实例会被装箱为对象,以便能够通过this指针将对一个堆对象的引用传给基方法。

但在调用非虚的、继承的方法时(比如GetType或MemberwiseClone),无论如何都要对值类型进行装箱。因为这些方法由System.Object定义,要求this实参是指向堆对象的指针。

此外,当将一个值类型转换为它实现的接口时,值类型会被装箱,因为接口变量需要引用堆上的对象。例如:

using System;

interface IDisplay
{
    void Display();
}

struct MyStruct : IDisplay
{
    public void Display()
    {
        Console.WriteLine("Displaying MyStruct");
    }
}

class Program
{
    static void Main()
    {
        MyStruct myStruct = new MyStruct();
        
        // 直接调用,不会装箱
        myStruct.Display();

        // 转换为接口,会发生装箱
        IDisplay displayable = myStruct;
        displayable.Display();
    }
}

有的语言(比如C++/CLI)允许更改已装箱值类型中的字段,但C#不允许。不过我们可以通过接口的方式绕过C#的限定,例如:

public class Program
{
    public interface IChangeBox 
    {
        void Change(int x);
    }

    public struct Box : IChangeBox
    {
        public int target;

        public void Change(int x)
        {
            target = x;
        }

        public override string ToString()
        {
            return target.ToString();
        }
    }

    static void Main(string[] args)
    {
        Box box;
        box.target = 1;

        object o = box;
        Console.WriteLine(o); // 输出:1
        ((IChangeBox)o).Change(2);
        Console.WriteLine(o); // 输出:2
    }
}

对象相等性和同一性

System.Object类型提供了名为Equals的虚方法,作用是在两个对象包含相同值的前提下返回true,它的实现如下:

public class Object
{
	public virtual Boolean Equals(Object obj)
	{
		if(this == obj) return true;
		return false;
	}
}

乍一看,似乎挺合理,但仔细思考能够发现,对于Object的Equals方法的默认实现,它实现的实际是同一性(identity),而非相等性(equality)。作者给出一个思路,来正确的实现Equals函数:

  1. 如果obj实参为null,就返回false。
  2. 如果this和obj实参引用同一个对象,就返回true。在比较包含大量字段的对象时,这一步有助于提升性能。
  3. 如果this和obj实参引用不同类型的对象,就返回false。
  4. 针对类型定义的每个实例字段,将this对象中的值与obj对象中的值进行比较。任何字段不相等,就返回false。
  5. 调用基类的Equals方法来比较它定义的任何字段。如果基类的Equals方法返回false,就返回false;否则返回true。

由于类型能够重写Object的Equals方法,所以不能再用它测试同一性。为了解决这个问题,Object提供了静态方法ReferenceEquals,其原型如下:

 public static bool ReferenceEquals(object objA, object objB) => objA == objB;

检查同一性(看两个引用是否指向同一个对象)务必调用ReferenceEquals,不应使用C#的==操作符(除非先把两个操作数都转型为Object),因为某个操作数的类型可能重载了==\操作符,为其赋予不同于“同一性”语义。

可以看出,在涉及对象相等性和同一性的时候,.NET Framework的涉及很容易使人混淆。不过System.ValueType就重写了Object的Equals方法,并进行了正确的实现来执行值的相等性检查(而不是同一性)。ValueType的Equals内部实现如下:

  1. 如果obj实参为null,就返回false。
  2. 如果this和obj实参引用不同类型的对象,就返回false。
  3. 针对类型定义的每个实例字段,都将this对象中的值与obj对象中的值进行比较(通过调用字段的Equals方法)。任何字段不相等,就返回false。
  4. 返回true。ValueType的Equals方法不调用Object的Equals方法。

在内部,ValueType的Equals方法利用反射来完成步骤3。由于CLR反射机制慢,定义自己的值类型时应重写Equals方法来提供自己的实现,从而提高用自己类型实例进行值相等性比较的性能(注意:自己的实现不调用base.Equals)。

定义自己的类型时,重写的Equals要符合相等性的4个特征。

  1. Equals必须自反,x.Equals(x)肯定返回true。
  2. Equals必须对称,x.Equals(y)和y.Equals(x)要返回相同的值。
  3. Equals必须可传递,x.Equals(y) = true,y.Equals(z) = true,则x.Equals(z)肯定要返回true。
  4. Equals必须一致,比较的两个值不变,Equals返回值(true或false)也不能变。

除此之外,重写Equals方法时,还需要做如下几件事:

  • 让类型实现System.IEquatable<T>接口的Equals方法
    这个泛型接口允许定义类型安全的Equals方法。通常,你实现的Equals方法应获取一个Object参数,以便在内部调用类型安全的Equals方法。
  • 重载==和!=操作符方法
    通常应实现这些操作符方法,在内部调用类型安全的Equals。

对象哈希码

FCL的设计者认为,如果能将任何对象的任何实例放到哈希表集合中,能带来很多好处。为此,System.Object提供了虚方法GetHashCode,它能获取任意对象的Inte32哈希码。

如果你定义的类型重写了Equals方法,还应重写GetHashCode方法,否则编译器会发出警告。类型定义Equals之所以还要定义GetHashCode,是由于在System.Collections.Hashtable类型,System.Collections.Generic.Dictionary类型以及其他一些集合的实现中,要求两个对象必须具有哈希码才被视为相等。

简单来说,向集合添加键/值对,首先要获取对象的哈希码。该哈希码指出键/值对要存储到哪个哈希桶(bucket)中。集合需要查找键时,会获取指定键对象的哈希码。该哈希码标识了现在要以顺序方式搜索的哈希桶,将在其中查找与指定键对象相等的键对象。采用这个算法来存储和查找键,意味着一旦修改了集合中的一个键对象,集合就再也找不到该对象。所以,需要修改哈希表中的键对象时,正确做法是移除原来的键/值对,修改键对象,再将新的键/值对添加回哈希表。

选择算法来计算类型实例的哈希码时,需要遵守以下规则:

  • 这个算法要提供良好的随机分布,使哈希表获得最佳性能。
  • 可在算法中调用基类的GetHashCode方法,并包含它的返回值。但一般不要调用Object或ValueType的GetHashCode方法,因为两者的实现都与高性能哈希算法“不沾边”。
  • 算法至少使用一个实例对象。
  • 理想情况下,算法使用的字段应该是不可变(immutable);也就是说,字段应在对象构造时初始化,在对象生存期“永不言变”。
  • 算法执行速度尽量快。
  • 包含相同值的不同对象应返回相同哈希码。例如,包含相同文本的两个String对象应返回相同哈希码。
  • 千万不要对哈希码持久化,因为哈希码很容易改变。例如,一个类型未来版本可能使用不同的算法来计算对象哈希码。

标签:via,C#,System,Equals,对象,实例,类型,装箱
From: https://www.cnblogs.com/chenxiayun/p/18338198

相关文章

  • 数据类型的转换
    目录导言一、隐式类型转换1.整型隐式类型转换2.浮点型隐式类型转换3.字符型隐式类型转换4.布尔型隐式类型转换二、显式类型转换1.整型显式类型转换2.引用数据类型显式类型转换父类和子类之间的转换接口和实现类之间的转换三、类型转换的注意事项1.数据溢出和精度丢失2.强......
  • GBNC 题解
    GBNC题解这可比平时做的红题难吧。题目以思维为主,和CCF的趋势有点挂钩。题目实际难度:橙-,橙,橙,黄。不知道从哪听到的消息,说你们的信息思维都特别好,所以就没有大红题了。T1思路这道题我们能用模拟来写。我们可以将这整个询问分为\(n\)个小询问。每次询问我们用一个整形\(......
  • IRAP图像类型
    视频序列里包含不同类型的图像,I帧(只使用帧内预测)、P帧(单向帧间预测)、B帧(双向帧间预测)。由于P帧和B帧在帧间预测时要参考其他帧,所以形成了不同帧间的时域依赖关系(时域预测帧必须在其参考帧解码后才能解码)。 上图展示了不同帧间的时域依赖关系。图上数字是编解码顺序,从左到右是......
  • c++两人组合账号
    周小灵彤:周小灵彤(六年级)-CSDN博客王小灵弘:wanghongyv-CSDN博客这是联合账号,因为我们俩懒太勤快了(嘻嘻)                                                     ......
  • ICASSP2025重要时间节点
    ICASSP2025重要时间节点征稿启事下载论文征集提交门户将于2024年8月12日(美国太平洋时间)开放。任何人都可以成为不超过九份投稿的作者团队的成员。**这50第IEEE声学、语音和信号处理国际会议(ICASSP)**将于2025年4月6日至11日在印度海得拉巴海得拉巴国际......
  • AutoCAD软件下载+安装+软件最新版2023中文版下载安装CAD2022
    纯净直装全版本(包含2023最新版)软件地址:rj.heihuyingyuan.comAutoCAD是美国Autodesk公司开发的一款computeraideddesign,即计算机辅助设计软件。它主要用于二维描绘和三维建模设计。AutoCAD的主要功能包括:1.二维绘图-可以绘制平面图形,进行几何构建和尺寸标注。......
  • AutoCAD Electrical2023 AutoCAD电气版软件下载安装-亲测可用
    AutoCADElectrical是Autodesk公司推出的一款专门用于电气工程设计的AutoCAD垂直解决方案。它在AutoCAD的CAD平台上,集成了强大的电气设计和智能化功能。纯净直装全版本(包含2023最新版)软件地址: http://321.pwAutoCADElectrical的主要功能包括:-电气符号库-内置完整......
  • 『模拟赛』暑假集训CSP提高模拟19
    Rank小挂,还好。A.数字三角形原[CF1517C]Fillomino2锣鼓Rmj炸了所以挂cf链接。签。倒叙考虑,优先向下,到底或者下面有数就向右,有正确性,复杂度\(\mathcal{O(n^2)}\)。水了篇题解,点点推荐rp++。点击查看代码#include<bits/stdc++.h>constintRatio=0;cons......
  • 【grpcurl】使用grpcurl测试GRPC服务
    一、场景   由于我们需要访问GRPC服务的方法,便于我们进行测试,所以我们开启了grpc服务的反射机制 二、安装grpcurl   https://github.com/fullstorydev/grpcurl   https://github.com/fullstorydev/grpcurl/releases下载对应环境的包即可sudodpkg-igrpcurl......
  • C语言学习心得-二维数组
    (一)二维数组的定义和初始化定义二维数组arr[3][5]:intarr[3][5]={{1,2,3,4,5},{2,3,4,5,6},{3,4,5,6,7}};仔细看这个数组arr[0] 是第一个一维数组,包含元素 arr[0][0],arr[0][1],arr[0][2],arr[0][3],arr[0][4]arr[1] 是第二个一维数组,包含元素 ......