RAD Studio 10.4 for delphi XE Assigned和Nil的联系与区别

RAD Studio 10.4 for delphi XE Assigned和Nil的联系与区别

一、原理

系统级别的函数System.pas  : System.Assigned
    function Assigned(var P): Boolean;

1、用于测试值为nil(未赋值的unassigned)的指针Pointer或方法变量procedural variable。
2、使用Assigned来确定指针Pointer或被Assigned(var P)中的方法变量function:P或procedure(var P)引用的方法是否为空。P必须是变量类型(var)的指针或方法类型(function或procedure)的引用。
3、Assigned(P)相当于测试var P:Pointer指针变量P<>nil,@P<>nil相当于测试function:P或procedure(var P)的P<>nil。
4、如果P=Nil,Assigned返回False,否则返回True。
5、特别注意

5.1、当测试对象事件object events不能测试其是否nil,此时使用Assigned是正确的方法。

5.2、关于方法指针与过程指针:(1)、无论方法指针还是过程指针,均能有效测试@P=nil或@P<>nil;(2)、方法指针既能有效测试Assigned(@P)或not Assigned(@P),也能有效测试@P=nil或@P<>nil;(3)、过程指针不能有效测试Assigned(@P)或not Assigned(@P),但能有效测试@P=nil或@P<>nil。

二、使用方法案例

1、对象事件object event、方法指针与过程指针

1.1、对象事件object events

var
  LNotifyEvent: TNotifyEvent;  //:事件对象
begin
  if LNotifyEvent <> nil then    {这种写法是错误的:会产生编译错误 }
    Writeln('这种写法是错误的:会产生编译错误');
  if Assigned(LNotifyEvent) then {这种写法是才是正确的 }
    Writeln('这种写法是才是正确的');
end;

         对象的通知事件的赋值:

 type
   TNotifyEvent = procedure(Sender: TObject) of object;
   TMainForm = class(TForm)
     procedure ButtonClick(Sender: TObject);
      //...
   end;
 var
   MainForm: TMainForm;
   OnClick: TNotifyEvent;

 OnClick := MainForm.ButtonClick;  //:通知事件的赋值

 1.2、方法指针与过程指针

        概念:

type
  TProcedureMyProc = procedure;          //:过程类型
  TMethodMyMethod = procedure of object; //:方法类型
var
  FmyProc:TProcedureMyProc;   //:过程指针变量
  FmyMethod:TMethodMyMethod;  //:方法指针变量
var
  FmyFunc_Pointer:Pointer;    //:普通指针变量

        关于方法指针与过程指针:(1)、无论方法指针还是过程指针,均能有效测试@P=nil或@P<>nil;(2)、方法指针既能有效测试Assigned(@P)或not Assigned(@P),也能有效测试@P=nil或@P<>nil;(3)、过程指针不能有效测试Assigned(@P)或not Assigned(@P),但能有效测试@P=nil或@P<>nil。官方解释原文:

        http://docwiki.embarcadero.com/RADStudio/Rio/en/Procedural_Types_(Delphi)#Method_Pointers 

         试验后确实如此:

type
  TProcedureMyProc = procedure;          //:过程类型
  TMethodMyMethod = procedure of object; //:方法类型
var
  FmyProc:TProcedureMyProc;   //:过程指针变量
  FmyMethod:TMethodMyMethod;  //:方法指针变量
var
  FmyFunc_Pointer:Pointer;    //:普通指针变量

function myFunc(var APointer:Pointer):Pointer;  //:函数:可供方法指针或过程指针调用,当然也可供方法或过程调用
       //var LAPointer:^Integer;
begin  //LAPointer:=Pointer(1);
  if APointer=nil then Result:=nil
  else Result:=APointer;
end;

procedure myProc;             //:过程
var LRef:Integer;
begin
  LRef:=1;
  @FmyProc:=@myProc;  //:@myProc等价于:Pointer(myProc);
  FmyProc:=myProc;
  FmyFunc_Pointer:=myFunc(@FmyProc);  //FmyProc:=nil;
end;

function myMethod :string;    //:函数:可供方法指针或过程指针调用,当然也可供方法或过程调用
var LRef:Integer;
    LStr:string;
begin
  LRef:=1; LStr:='AaBb方法指针。,.,';
  Result:=LStr;
end;

procedure TForm2.FormShow(Sender: TObject);
var LmyProcPointerValue:Integer;
begin
  Memo_Test.Lines.Add('一、方法指针:');
  Memo_Test.Lines.Add('    比如1、:方法:procedure myProc;');
  if Assigned(@myProc) then
    Memo_Test.Lines.Add('    返回Assigned(@myProc)=true,方法指针能识别Assigned(@myProc),内存中的内容: '+PChar(@myProc) );
  if @myProc<>nil then
    Memo_Test.Lines.Add('    返回(@myProc<>nil)=true,方法指针能识别@myProc<>nil,内存中的内容: '+PChar(@myProc) );

  @FmyMethod:=Pointer(myMethod);
  if    ( (@myMethod<>nil) and (@FmyMethod<>nil) )
    and ( Assigned(@myMethod) and Assigned(@FmyMethod) ) then
  Memo_Test.Lines.Add('    比如2、:方法类型:type TMethodMyMethod = procedure of object;'+sLineBreak
                     +'            方法指针变量:var FmyMethod:TMethodMyMethod;'+sLineBreak
                     +'            赋值方法指针:@FmyMethod:=Pointer(myMethod);'+sLineBreak
                     +'            方法指针变量:var FmyMethod:TMethodMyMethod;'+sLineBreak
                     +'    返回方法指针所指的字符串的数值:'+PChar(@FmyMethod)
  ) else Memo_Test.Lines.Add('    不返回任何结果');


  Memo_Test.Lines.Add('二、过程指针,比如:');
  Memo_Test.Lines.Add(
     '    TProcedureMyProc = procedure;'+sLineBreak
    +'    var FmyProc:TProcedureMyProc;'+sLineBreak
    +'    Procedure pointer types are always incompatible with method pointer types.'+sLineBreak
    +'    The value nil can be assigned to any procedural type:'+sLineBreak
    +'    过程指针类型始终与方法指针类型不兼容。值nil可以分配给任何过程类型。比如: '+sLineBreak
    +'      if Assigned(FmyProc) then  //: 过程不能识别Assigned(FmyProc)'+sLineBreak
    +'      if Assigned(@FmyProc) then //: 过程指针也不能识别Assigned(@FmyProc)'+sLineBreak
    +'      if FmyProc=nil then        //: 过程不能识别FmyProc=nil且编译器会报错'+sLineBreak
    +'      if @FmyProc=nil then       //: 但过程指针能识别@FmyProc=nil'+sLineBreak
  );
  if @FmyProc=nil then         //: 但过程指针能识别@FmyProc=nil
  begin
    Memo_Test.Lines.Add('      @FmyProc=nil');
    myProc;
  end else Memo_Test.Lines.Add('      过程FmyProc及过程指针@FmyProc均不能识别Assigned');
  if FmyFunc_Pointer<>nil then //: 过程指针变量也能识别@FmyProc=nil
    Memo_Test.Lines.Add('      FmyFunc_Pointer<>nil,过程指针变量的内存值: '+PChar(@FmyFunc_Pointer) );
  if (@myProc<>nil) and (@FmyFunc_Pointer<>nil)  then
  begin
    Memo_Test.Lines.Add('      @myProc<>nil,过程指针内存值: '+PChar(@myProc) );
    Memo_Test.Lines.Add('      @myProc<>nil,过程指针变量内存值: '+PChar(@FmyFunc_Pointer) );
  end;
  Memo_Test.Lines.Add('三、结论:');
  Memo_Test.Lines.Add(
     '    1、无论方法指针还是过程指针,均能有效测试@P=nil或@P<>nil'+sLineBreak
    +'    2、方法指针既能有效测试Assigned(@P)或not Assigned(@P),也能有效测试@P=nil或@P<>nil'+sLineBreak
    +'    3、过程指针不能有效测试Assigned(@P)或not Assigned(@P),但能有效测试@P=nil或@P<>nil'
  );

end;

2、方法或过程的指针变量的回调procedural variable : function myFuc(var APointer: Pointer):Pointer;

function myFuc(var APointer: Pointer):Pointer;
begin
  if APointer=nil then Result:=nil
  else Result:=APointer;
end;
//:procedure AProc(var AItem: Pointer);类似

var LPointer: Pointer;        //:声明:普通的指针变量
begin
  LPointer:=nil;
  if myFuc(LPointer)=nil then
    Label_1.Text:=('myFuc(LPointer)=nil');      //:返回'myFuc(LPointer)=nil'

  //赋值普通的指针变量:注意:
  GetMem(LPointer, 1024);	  //LPointer有效且赋值
  FreeMem(LPointer, 1024);	//LPointer不再有效,但其仍然<>nil
  if myFuc(LPointer)=nil then
    Label_2.Text:=('myFuc(LPointer)=nil')
  else Label_2.Text:=('myFuc(LPointer)<>nil');  //:返回'myFuc(LPointer)<>nil'
  LPointer:=nil;
end;


3、普通类型的变量 

不能针对普通类型的变量使用Assigned或nil,比如:
var LVar1 :String; LVar2 :Double;
begin
  if Assigned(LVar1 ) then Writeln('这种写法是错误的:会产生编译错误');
  if Assigned(LVar2 ) then Writeln('这种写法是错误的:会产生编译错误');
  if (LVar1 =nil) then Writeln('这种写法是错误的:会产生编译错误');
  if (LVar2 =nil) then Writeln('这种写法是错误的:会产生编译错误');
end;

4、普通的指针变量

var LPointer: Pointer;        //:声明:普通的指针变量
begin
  if (LPointer = nil) then
    Label1.Text:=('这种写法是正确的:因为LPointer已被声明为普通的指针变量;但if返回false:因为虽然LPointer还没有赋值,但LPointer已经申明了指针变量var LPointer: Pointer;');
  if (LPointer <> nil) then
    Label2.Text:=('这种写法是正确的:因为LPointer已被声明为普通的指针变量;且if返回true:因为虽然LPointer还没有赋值,但LPointer已经申明了指针变量var LPointer: Pointer;');
  if Assigned( LPointer ) then
    Label3.Text:=('这种写法是正确的:因为LPointer已被声明为普通的指针变量,且返回true:因为LPointer已经申明了指针变量var LPointer: Pointer;');
  if not Assigned( LPointer ) then
    Label4.Text:=('这种写法是正确的:因为LPointer已被声明为普通的指针变量,但返回false:因为LPointer已经申明了指针变量var LPointer: Pointer;');
  //赋值普通的指针变量:注意:
  LPointer:=nil;
    //:普通指针变量赋值:=nil相当于对象TObject.DisposeOf从内存中FreeAndNil,其声明也抹掉啦
    //:但不能FreeAndNil(LPointer);运行时因为ARC自动引用计数会报内存地址错误,只能再次赋值LPointer:=nil;
  if (LPointer = nil) then
    Label5.Text:=('这种写法是正确的:因为LPointer为普通的指针变量,且已经被赋值为nil');
  if Assigned( LPointer ) then
    Label6.Text:=('这种写法是正确的:因为LPointer为普通的指针变量,但返回false:普通指针变量:=nil相当于对象TObject.DisposeOf从内存中FreeAndNil,其声明也抹掉啦');
  //赋值普通的指针变量:注意:
  GetMem(LPointer, 1024);	  //LPointer有效且赋值
  FreeMem(LPointer, 1024);	//LPointer不再有效,但其仍然<>nil
  if Assigned( LPointer ) then
    Label7.Text:=('这种写法是正确的:因为LPointer已被声明为普通的指针变量,且返回true:因为LPointer已经申明了指针变量var LPointer: Pointer;且已经被赋值,内存中既有指针起始位置又有内存空间GetMem(LPointer, 1024);');
  if ( LPointer <>nil ) then
    Label8.Text:=('这种写法是正确的:因为LPointer已被声明为普通的指针变量,且返回true:因为LPointer已经申明了指针变量var LPointer: Pointer;且已经被赋值,内存中既有指针起始位置又有内存空间GetMem(LPointer, 1024);');
  LPointer:=nil;//:FreeAndNil(LPointer);是错误的,只能再次赋值LPointer:=nil;
end;

5、记录(结构体)化的指针

案例System.TypInfo.pas

  PPropInfo = ^TPropInfo;    //:记录(结构体)化的指针变量
  TPropInfo = packed record  //:记录(结构体)
    PropType: PPTypeInfo;
    GetProc: Pointer;        //:普通指针变量
    SetProc: Pointer;        //:普通指针变量
    StoredProc: Pointer;     //:普通指针变量
    Index: Integer;
                                                                                 
    Default: Integer;
    NameIndex: SmallInt;
    Name: TSymbolName;
    function NameFld: TTypeInfoFieldAccessor; inline;
    function Tail: PPropInfo; inline;  //:返回记录(结构体)化的指针变量的函数
  end;

procedure FreeAndNilProperties(AObject: TObject);
var                     //:变量定义
  I, Count: Integer;
  PropInfo: PPropInfo;  //:记录(结构体)化的指针变量
  TempList: PPropList;
  LObject: TObject;     //:对象类型的变量
begin
  Count := GetPropList(AObject, TempList);
  if Count > 0 then
  try
    for I := 0 to Count - 1 do
    begin
      PropInfo := TempList^[I];
      if (PropInfo^.PropType^.Kind = tkClass) 
         and Assigned(PropInfo^.GetProc) 
           //:正确的Assigned写法:因为PropInfo^.GetProc返回GetProc:Pointer;普通指针变量
         and Assigned(PropInfo^.SetProc) 
           //:正确的Assigned写法:因为PropInfo^.SetProc返回SetProc:Pointer;普通指针变量
      then
      begin
        LObject := GetObjectProp(AObject, PropInfo);
        if LObject <> nil then 
           //:正确的nil写法:因为LObject:TObject;是对象类型的变量而非普通变量
        begin
          SetObjectProp(AObject, PropInfo, nil);
          LObject.Free;
        end;
      end;
    end;
  finally
    FreeMem(TempList);
  end;
end;

三、官方英文参考:

http://docwiki.embarcadero.com/Libraries/Rio/en/System.Assigned

http://docwiki.embarcadero.com/CodeExamples/Rio/en/SystemAssigned_(Delphi)

http://docwiki.embarcadero.com/RADStudio/Rio/en/Delphi_Intrinsic_Routines

四、关于窗体组件对象测试Assigned和Nil、更安全的Create和更安全的DisposeOf释放:

        官方英文参考:

http://docwiki.embarcadero.com/RADStudio/Rio/en/Procedural_Types_(Delphi)#Method_Pointers

http://docwiki.embarcadero.com/RADStudio/Rio/en/Program_Control_(Delphi)

http://docwiki.embarcadero.com/RADStudio/Rio/en/Pointers_and_Pointer_Types_(Delphi)#Overview_of_pointers

4.1、窗体的对象继承关系

TCommonCustomForm = class(TFmxObject)  //FMX.Forms.pas
  TFmxObject = class(TComponent)       //FMX.Types.pas
    TComponent = class(TPersistent)    //System.Classes.pas
      TPersistent = class(TObject)     //System.pas
        TObject = class                //System.pas

4.2、窗体对象将其父类的继承解构BeforeDestruction

procedure TCommonCustomForm.BeforeDestruction;

  procedure SaveStateNotifyCheck;
  var
    SaveStateService: IFMXSaveStateService;
  begin
    if not TPlatformServices.Current.SupportsPlatformService(IFMXSaveStateService, SaveStateService) or
      not SaveStateService.Notifications then
      SaveStateHandler(Self, nil);
  end;

var
  I: Integer;
begin
  SaveStateNotifyCheck;
  if Assigned(FOnDestroy) then
    FOnDestroy(Self);
  for I := 0 to ChildrenCount - 1 do
    if Children[I] = Owner then
    begin
      Children[I].Parent := nil;
      Break;
    end;
  inherited;
end;

        TCommonCustomForm及其父类解构前,均对其子组件及其自身做:=nil处理,同时对其状态通知、状态保存句柄做:=nil做:=nil处理、以及对“观察者”订阅与发布事件及其监听等等做:=nil处理。

//TCommonCustomForm的父类TFmxObject:

procedure TFmxObject.BeforeDestruction;
var
  I: Integer;
  L2: TList<Pointer>;
begin
  { NotifyList }
  if FNotifyList <> nil then
  begin
    L2 := TList<Pointer>.Create;
    try
      L2.AddRange(FNotifyList);
      for I := L2.Count - 1 downto 0 do
        if FNotifyList.Contains(L2[I]) then
          IFreeNotification(L2[I]).FreeNotification(Self);
          {  IFreeNotification = interface
              ['{FEB50EAF-A3B9-4b37-8EDB-1EF9EE2F22D4}']
              procedure FreeNotification(AObject: TObject);
            end;  }  //释放通知事件做:=nil处理
    finally
      L2.Free;
      FreeAndNil(FNotifyList);  //释放通知事件列表FreeAndNil做:=nil处理
    end;
  end;
  inherited;
end;

TCommonCustomForm->TFmxObject的父类TComponent:

procedure TComponent.BeforeDestruction;
begin
  if not (csDestroying in ComponentState) then
    Destroying;
end;
procedure TComponent.Destroying;
var
  I: Integer;
begin
  if not (csDestroying in FComponentState) then
  begin
    Include(FComponentState, csDestroying);
    if FComponents <> nil then  //:测试组件做=nil处理
      for I := 0 to FComponents.Count - 1 do
        TComponent(FComponents[I]).Destroying;  //:测试子组件做=nil处理
  end;
end;
procedure TComponent.DestroyComponents;  
var
  Instance: TComponent;
begin
  FreeAndNil(FSortedComponents);
  while FComponents <> nil do   //:正式移除组件:测试组件做=nil处理
  begin
    Instance := FComponents.Last;
    if (csFreeNotification in Instance.FComponentState)
      or (FComponentState * [csDesigning, csInline] = [csDesigning, csInline]) then
      RemoveComponent(Instance) //:正式移除组件:测试组件做=nil处理
    else
      Remove(Instance);  //:正式移除组件:测试组件做=nil处理
    Instance.DisposeOf;  //:组件实例做=nil处理
  end;
end;
procedure TComponent.Remove(AComponent: TComponent);
var
  Count: Integer;
begin
  AComponent.FOwner := nil;   //:正式移除组件:测试组件做=nil处理
  Count := FComponents.Count;
  if Count > 0 then
  begin
    { On destruction usually the last item is deleted first }
    if FComponents[Count - 1] = AComponent then
      FComponents.Delete(Count - 1)
    else
      FComponents.Remove(AComponent);

    if FSortedComponents <> nil then
      FSortedComponents.Remove(AComponent);
  end;
  if FComponents.Count = 0 then
  begin
    FreeAndNil(FSortedComponents); //:正式移除组件:测试组件做=nil处理
    FreeAndNil(FComponents);       //:正式移除组件:测试组件做=nil处理
  end;
end;

//System.SysUtils.pas    //System.SysUtils.FreeAndNil
procedure FreeAndNil(var Obj);  //:无论是否使用引用计数均做=nil处理
{$IF not Defined(AUTOREFCOUNT)}
var
  Temp: TObject;
begin
  Temp := TObject(Obj);
  Pointer(Obj) := nil;     //做=nil处理
  Temp.Free;
end;
{$ELSE}
begin
  TObject(Obj) := nil;     //做=nil处理
end;
{$ENDIF}


//System.pas
procedure TObject.DisposeOf;
type
  TDestructorProc = procedure (Instance: Pointer; OuterMost: ShortInt);
begin
{$IFDEF AUTOREFCOUNT}
  if Self <> nil then
  begin
    Self.__ObjAddRef; // Ensure the instance remains alive throughout the disposal process
    try
      if __SetDisposed(Self) then
      begin
        _BeforeDestruction(Self, 1);
        TDestructorProc(PPointer(PByte(PPointer(Self)^) + vmtDestroy)^)(Self, 0);
      end;
    finally
      Self.__ObjRelease; // This will deallocate the instance if the above process cleared all other references.
    end;
  end;
{$ELSE}
  Free;
{$ENDIF}
end;


4.3、安全的条件测试Create和Close窗体

function TfmxTestGYListview1.AppEvent(AAppEvent: TApplicationEvent; AContext: TObject): Boolean;
var LAppEventStatus :string;
begin
  Result := true;
  //(*
  case AAppEvent of
    TApplicationEvent.FinishedLaunching:
      begin
        //Memo1.Lines.Add('应用完成系统载入:');
      end;
    TApplicationEvent.WillTerminate:
      begin
        //Memo1.Lines.Add('应用将要中断');
        {$IFDEF MSWINDOWS} //Windows下防止用户强行中断正在Rest响应数据的应用,否则内存泄漏:
        if IFutureCtL00001<>nil then
          if IFutureCtL00001.Status<>TTaskStatus.Completed then IFutureCtL00001.Wait(500);
        if FITask<>nil then
          if FITask.Status<>TTaskStatus.Completed then FITask.Wait( 2000 );  
        {$ENDIF}
      end;

    TApplicationEvent.EnteredBackground:
      begin
        //:也许10.4 for Android10越来越严格约束:
        //:按了返回键若程序不控制则立即进入后台:
        //:而进入后台:主程序就被:=nil掉了
        //:(nil就相当于对象 TObject.disposeOf = FreeAndNil(TObject)  )
        //:FreeAndNil是10.4与10.3.3的一个变化
        TimerGlobal.Enabled := false;
        TimerGuangGao.Enabled := false;
        ///////////已进入后台千千万不与UI有任何交互,否则异常:////////////////////////////////////////////////////////
          //Memo1.Lines.Add('应用已进入后台');
          //Text_0201.Text:=TInterlocked(FRestSearchFrequency).ToString;
      end;
    TApplicationEvent.WillBecomeInactive:
      begin
        TimerGlobal.Enabled := false;
        TimerGuangGao.Enabled := false;
        {Memo1.Lines.Add(
          'App切入后台再重新调入过程:'+sLineBreak
          +'1、应用将变为非活动状态:'+sLineBreak
          +string('').PadLeft(8,#32)+'此时就需提前关闭任何'+sLineBreak
          +string('').PadLeft(8,#32)+'与UI相关的线程与计时,'+sLineBreak
          +string('').PadLeft(8,#32)+'否则手机重新切到前台'+sLineBreak
          +string('').PadLeft(8,#32)+'会内部异常!');
        Memo1.Lines.Add(
          '*注意:已进入后台事件中:'
          +string('').PadLeft(8,#32)+'千万不能有与UI相关的'+sLineBreak
          +string('').PadLeft(8,#32)+'任何交互、线程与计时'+sLineBreak
          +string('').PadLeft(8,#32)+'否则手机重新切到前台'+sLineBreak
          +string('').PadLeft(8,#32)+'亦会内部异常!'+sLineBreak
          );}
      end;
    TApplicationEvent.WillBecomeForeground:
      begin
        TimerGlobal.Enabled := true;
        if (FDMemTable1.Active=false) then LAppEventStatus:='但内存表不活动'
        else LAppEventStatus:='内存表活动';
        //Memo1.Lines.Add('2、应用将进入前台:'+LAppEventStatus+',单号'+FGetBillNo_Currt.Trim);

        TimerGuangGao.Enabled := true;
      end;
    TApplicationEvent.BecameActive:
      begin
        TimerGlobal.Enabled := true;
        if (FDMemTable1.Active=false) then LAppEventStatus:='但内存表不活动'
        else LAppEventStatus:='内存表活动';
        //Memo1.Lines.Add('3、应用已变为活动状态:'+LAppEventStatus+',单号'+FGetBillNo_Currt.Trim);

        TimerGuangGao.Enabled := true;
          //TranslucentStatusAndNavBar(self, Rectangle_02);
          // 将状态栏与导航栏透明显示,顺便做一些每个窗口必须要做的处理
          //Rectangle_02.Height := Rectangle_02.Height - 25; // 因为上面多加了25
      end;
    TApplicationEvent.TimeChange:
      begin
        //Memo1.Lines.Add('应用计时改变:');
      end;
    TApplicationEvent.LowMemory:
      begin
        //Memo1.Lines.Add('系统报应用高内存耗用:');
      end;
    TApplicationEvent.OpenURL:
      begin
        //Memo1.Lines.Add('App打开一个URL:');
      end;
  end; //*)
end;

        所以,更科学和安全的写法:

  if DataModuleSqlLiteCommon=nil then DataModuleSqlLiteCommon
    :=TDataModuleSqlLiteCommon.Create(Application);
  //:if DataModuleSqlLiteCommon=nil比if not Assigned(DataModuleSqlLiteCommon)更安全
  //另外:
    //:最好由于Application来管理一切窗体://:=TDataModuleSqlLiteCommon.Create(AFormOrFrame);
    //:DataModuleSqlLiteCommon的Owner:AFormOrFrame万一已在某个事件被:=nil;掉了

喜欢的话,就在下面点个赞、收藏就好了,方便看下次的分享:

猜你喜欢

转载自blog.csdn.net/pulledup/article/details/107112785