Delphi:如何将接口实现委托给子对象? [英] Delphi: How delegate interface implementation to child object?

查看:35
本文介绍了Delphi:如何将接口实现委托给子对象?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我有一个对象,它将一个特别复杂的接口的实现委托给一个子对象.这个正是我认为是TAggregatedObject的工作.child"对象维护对其controller"和所有QueryInterface<的弱引用/code> 请求被传递回父级.这维护了 IUnknown 的规则总是同一个对象.

因此,我的父对象(即Controller")声明它实现了IStream 接口:

类型TRObot = 类(TInterfacedObject,IStream)私人的函数 GetStream: IStream;上市属性Stream:IStream读取GetStrem实现IStream;结尾;

<块引用>

注意:这是一个假设的例子.我选择了Robot这个词因为听起来很复杂,而且单词只有 5 个字母长 - 它是短的.我也选择了 IStream 因为它的短.我打算用IPersistFileIPersistFileInit,但它们更长,并使示例代码更难实现.其他词:这是一个假设的例子.

现在我有了将实现 IStream 的子对象:

类型TRObotStream = 类(TAggregatedObject,IStream)上市...结尾;

剩下的就是我的问题开始了:在需要时创建 RobotStream:

function TRObot.GetStream: IStream;开始结果 := TRobotStream.Create(Self) as IStream;结尾;

此代码无法编译,出现错误 Operator not 适用于该操作数类型..

这是因为 delphi 试图在没有实现 IUnknown 的对象上执行 as IStream:

TAggregatedObject = 类...{我未知}函数 QueryInterface(const IID: TGUID; out Obj): HResult;标准调用;函数_AddRef:整数;标准调用;函数_Release:整数;标准调用;...

IUnknown 方法 可能存在,但对象不宣传它支持IUnknown.如果没有 IUnknown 接口,Delphi 无法调用 QueryInterface 来执行转换.

所以我改变了我的 TRobotStream 类来宣传它实现了缺少的接口(它确实做到了;它从它的祖先那里继承了它):

类型TRObotStream = 类(TAggregatedObject,IUnknown,IStream)...

现在它编译了,但在运行时崩溃了:

Result := TRobotStream.Create(Self) as IStream;

现在我可以看到发生了什么,但我无法解释为什么.Delphi 正在调用 IntfClear,在我的父 Robot 对象上,在离开子对象的构造函数的路上.

我不知道防止这种情况的正确方法.我可以尝试强制演员:

Result := TRobotStream.Create(Self as IUnknown) as IStream;

并希望保留参考.事实证明它确实保留了引用 - 在退出构造函数的过程中没有崩溃.

<块引用>

注意:这让我很困惑.由于我正在传递一个对象,其中需要界面.我会假设编译器是隐式的执行类型转换,即:

Result := TRobotStream.Create(Self as IUnknown);

为了满足呼唤.这事实上,语法检查器没有抱怨让我假设一切都是正确.

<小时>

但崩溃还没有结束.我已将行更改为:

Result := TRobotStream.Create(Self as IUnknown) as IStream;

代码确实从 TRobotStream 的构造函数返回,而不会破坏我的父对象,但现在我得到了堆栈溢出.

原因是 TAggregatedObject 将所有 QueryInterface(即类型转换)推迟回父对象.就我而言,我将 TRobotStream 转换为 IStream.

当我在结尾处向 TRobotStream 询问其 IStream 时:

Result := TRobotStream.Create(Self as IUnknown) as IStream;

它转身向控制器请求IStream接口,这会触发对以下内容的调用:

Result := TRobotStream.Create(Self as IUnknown) as IStream;结果 := TRobotStream.Create(Self as IUnknown) as IStream;

它转身并呼叫:

Result := TRobotStream.Create(Self as IUnknown) as IStream;结果 := TRobotStream.Create(Self as IUnknown) as IStream;结果 := TRobotStream.Create(Self as IUnknown) as IStream;

繁荣! 堆栈溢出.

<小时>

盲目地,我尝试删除对 IStream 的最终转换,让 Delphi 尝试将对象隐式转换为接口(我刚刚在上面看到的操作不正确):

Result := TRObotStream.Create(Self as IUnknown);

现在没有崩溃;我不太明白这一点.我已经构建了一个对象,一个支持多个接口的对象.Delphi 现在知道如何转换接口了?它是否执行了正确的引用计数?我在上面看到它没有.是否有一个微妙的错误等待客户崩溃?

所以我有四种可能的方式来呼叫我的一条线路.其中哪一个是有效的?

  1. 结果 := TRObotStream.Create(Self);
  2. Result := TRObotStream.Create(Self as IUnknown);
  3. Result := TRobotStream.Create(Self) as IStream;
  4. Result := TRobotStream.Create(Self as IUnknown) as IStream;

真正的问题

我遇到了很多微妙的错误,并且难以理解编译器的复杂性.这让我相信我所做的一切都是完全错误的.如果需要,请忽略我所说的一切,并帮助我回答问题:

<块引用>

将接口实现委托给子对象的正确方法是什么?

也许我应该使用 TContainedObject 而不是 TAggregatedObject.也许两者协同工作,其中父项应为 TAggregatedObject,而子项为 TContainedObject.也许情况正好相反.也许在这种情况下都不适用.

<块引用>

注意:我帖子主要部分的所有内容都可以忽略.这只是以表明我已经考虑过了.有人会争辩说通过包括我尝试过的,我有毒害了可能的答案;相当比回答我的问题,人们可能会专注于我失败的问题.

真正的目标是委托接口子对象的实现.这问题包含我的详细尝试在解决问题时TAggregatedObject.你甚至不查看我的另外两个解决方案模式.其中之一患有圆形参考计数,并打破IUnknown 等价规则.

Rob Kennedy 可能还记得;并问我提出一个要求问题的解决方案,而不是我的一个问题的解决方案解决方案.

语法化

编辑 2: 没有机器人控制器这样的东西.嗯,有 - 我一直在使用 Funuc RJ2 控制器.但在这个例子中不是!

编辑 3*

 TRObotStream = class(TAggregatedObject, IStream)上市{ IStream }函数寻求(dlibMove:Largeint;dwOrigin:Longint;出 libNewPosition: Largeint): HResult;标准调用;函数 SetSize(libNewSize: Largeint): HResult;标准调用;function CopyTo(stm: IStream; cb: Largeint; out cbRead: Largeint; out cbWritten: Largeint): HResult;标准调用;函数提交(grfCommitFlags:Longint):HResult;标准调用;功能恢复:HResult;标准调用;函数 LockRegion(libOffset: Largeint; cb: Largeint; dwLockType: Longint): HResult;标准调用;函数 UnlockRegion(libOffset: Largeint; cb: Largeint; dwLockType: Longint): HResult;标准调用;函数 Stat(out statstg: TStatStg; grfStatFlag: Longint): HResult;标准调用;功能克隆(输出 stm:IStream):HResult;标准调用;函数读取(pv:指针;cb:Longint;pcbRead:PLongint):HResult;标准调用;函数写(pv:指针;cb:Longint;pcbWritten:PLongint):HResult;标准调用;结尾;TRObot = 类(TInterfacedObject,IStream)私人的FStream:TRObotStream;函数 GetStream: IStream;上市析构函数销毁;覆盖;属性Stream:IStream读取GetStream实现IStream;结尾;无功Form1:TForm1;执行{$R *.DFM}过程 TForm1.Button1Click(Sender: TObject);无功rs:IStream;开始rs := TRObot.Create;LoadRobotFromDatabase(rs);//dummy 方法,只是为了演示我们使用流rs := 零;结尾;程序 TForm1.LoadRobotFromDatabase(rs: IStream);开始rs.回复;//dummy 方法调用,只是为了证明我们可以调用它结尾;析构函数 TRObot.Destroy;开始FStream.Free;遗传;结尾;函数 TRObot.GetStream: IStream;开始如果 FStream = nil 那么FStream := TRObotStream.Create(Self);结果:= FStream;结尾;

这里的问题是父"TRobot 对象在调用过程中被破坏:

FStream := TRObotStream.Create(Self);

解决方案

你必须为创建的子对象添加一个字段实例:

类型TRObot = 类(TInterfacedObject,IStream)私人的FStream:TRObotStream;函数 GetStream: IStream;上市属性Stream:IStream读取GetStream实现IStream;结尾;析构函数 TRObot.Destroy;开始FStream.Free;遗传;结尾;函数 TRObot.GetStream: IStream;开始如果 FStream = nil 那么FStream := TRObotStream.Create(Self);结果:= FStream;结尾;

更新正如您已经猜到的那样,TRObotStream 应该从 TAggregatedObject 派生.声明应为:

类型TRObotStream = 类(TAggregatedObject,IStream)...结尾;

没有必要提到IUnknown.

在 TRobot.GetStream 中,行 result := FStream 做了一个隐式 FStream as IStream 所以写出来也没有必要.

FStream 必须声明为 TRobotStream 而不是 IStream,以便在销毁 TRobot 实例时可以销毁它.注意:TAggregatedObject 没有引用计数,所以容器必须照顾它的生命周期.

更新(Delphi 5 代码):

unit Unit1;界面用途Windows、消息、SysUtils、类、图形、控件、表单、对话框、StdCtrls、activex、comobj;类型TForm1 = 类(TForm)Button1:TButton;Edit1: TEdit;过程 Button1Click(Sender: TObject);私人的过程 LoadRobotFromDatabase(rs: IStream);上市结尾;类型TRObotStream = 类(TAggregatedObject,IStream)上市{ IStream }函数寻求(dlibMove:Largeint;dwOrigin:Longint;出 libNewPosition: Largeint): HResult;标准调用;函数 SetSize(libNewSize: Largeint): HResult;标准调用;function CopyTo(stm: IStream; cb: Largeint; out cbRead: Largeint; out cbWritten: Largeint): HResult;标准调用;函数提交(grfCommitFlags:Longint):HResult;标准调用;功能恢复:HResult;标准调用;函数 LockRegion(libOffset: Largeint; cb: Largeint; dwLockType: Longint): HResult;标准调用;函数 UnlockRegion(libOffset: Largeint; cb: Largeint; dwLockType: Longint): HResult;标准调用;函数 Stat(out statstg: TStatStg; grfStatFlag: Longint): HResult;标准调用;功能克隆(输出 stm:IStream):HResult;标准调用;函数读取(pv:指针;cb:Longint;pcbRead:PLongint):HResult;标准调用;函数写(pv:指针;cb:Longint;pcbWritten:PLongint):HResult;标准调用;结尾;类型TRObot = 类(TInterfacedObject,IStream)私人的FStream:TRObotStream;函数 GetStream: IStream;上市析构函数销毁;覆盖;属性Stream:IStream读取GetStream实现IStream;结尾;无功Form1:TForm1;执行{$R *.dfm}过程 TForm1.Button1Click(Sender: TObject);无功rs:IStream;开始rs := TRObot.Create;LoadRobotFromDatabase(rs);//dummy 方法,只是为了演示我们使用流rs := 零;结尾;程序 TForm1.LoadRobotFromDatabase(rs: IStream);开始rs.回复;//dummy 方法调用,只是为了证明我们可以调用它结尾;功能 TRObotStream.Clone(out stm: IStream): HResult;开始结尾;函数 TRObotStream.Commit(grfCommitFlags: Integer): HResult;开始结尾;function TRobotStream.CopyTo(stm: IStream; cb: Largeint; out cbRead, cbWritten: Largeint): HResult;开始结尾;函数 TRObotStream.LockRegion(libOffset, cb: Largeint; dwLockType: Integer): HResult;开始结尾;函数 TRobotStream.Read(pv: Pointer; cb: Integer; pcbRead: PLongint): HResult;开始结尾;功能 TRObotStream.Revert: HResult;开始结尾;函数 TRObotStream.Seek(dlibMove: Largeint; dwOrigin: Integer;出 libNewPosition: Largeint): HResult;开始结尾;函数 TRObotStream.SetSize(libNewSize: Largeint): HResult;开始结尾;函数 TRObotStream.Stat(out statstg: TStatStg; grfStatFlag: Integer): HResult;开始结尾;函数 TRObotStream.UnlockRegion(libOffset, cb: Largeint; dwLockType: Integer): HResult;开始结尾;函数 TRobotStream.Write(pv: Pointer; cb: Integer; pcbWritten: PLongint): HResult;开始结尾;析构函数 TRObot.Destroy;开始FStream.Free;遗传;结尾;函数 TRObot.GetStream: IStream;开始如果 FStream = nil 那么FStream := TRObotStream.Create(Self);结果:= FStream;结尾;结尾.

i have an object which delegates implementation of a particularly complex interface to a child object. This is exactly i think is the job of TAggregatedObject. The "child" object maintains a weak reference to its "controller", and all QueryInterface requests are passed back to the parent. This maintains the rule that IUnknown is always the same object.

So, my parent (i.e. "Controller") object declares that it implements the IStream interface:

type
   TRobot = class(TInterfacedObject, IStream)
   private
      function GetStream: IStream;
   public
      property Stream: IStream read GetStrem implements IStream;
   end;

Note: This is a hypothetical example. i chose the word Robot because it sounds complicated, and and word is only 5 letters long - it's short. i also chose IStream because its short. i was going to use IPersistFile or IPersistFileInit, but they're longer, and make the example code harder to real. In other words: It's a hypothetical example.

Now i have my child object that will implement IStream:

type
   TRobotStream = class(TAggregatedObject, IStream)
   public
      ...
   end;

All that's left, and this is where my problem starts: creating the RobotStream when it's asked for:

function TRobot.GetStream: IStream;
begin
    Result := TRobotStream.Create(Self) as IStream;
end;

This code fails to compile, with the error Operator not applicable to this operand type..

This is because delphi is trying to perform the as IStream on an object that doesn't implement IUnknown:

TAggregatedObject = class
 ...
   { IUnknown }
   function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall;
   function _AddRef: Integer; stdcall;
   function _Release: Integer; stdcall;
 ...

The IUnknown methods may be there, but the object doesn't advertise that it supports IUnknown. Without an IUnknown interface, Delphi can't call QueryInterface to perform the cast.

So i change my TRobotStream class to advertise that it implements the missing interface (which it does; it inherits it from its ancestor):

type
   TRobotStream = class(TAggregatedObject, IUnknown, IStream)
   ...

And now it compiles, but crashes at runtime on the line:

Result := TRobotStream.Create(Self) as IStream;

Now i can see what's happening, but i can't explain why. Delphi is calling IntfClear, on my parent Robot object, on the way out of the child object's constructor.

i don't know the proper way to prevent this. i could try forcing the cast:

Result := TRobotStream.Create(Self as IUnknown) as IStream;

and hope that keeps a reference. Turns out that it does keep the reference - no crash on the way out of the constructor.

Note: This is confusing to me. Since i am passing an object where an interface is expected. i would assume that the compiler is implicitly preforming a typecast, i.e.:

Result := TRobotStream.Create(Self as IUnknown);

in order to satisfy the call. The fact that the syntax checker didn't complain let me to assume all was correct.


But the crashes aren't over. i've changed the line to:

Result := TRobotStream.Create(Self as IUnknown) as IStream;

And the code does indeed return from the constructor of TRobotStream without destroying my parent object, but now i get a stack overflow.

The reason is that TAggregatedObject defers all QueryInterface (i.e. type casts) back to the parent object. In my case i am casting a TRobotStream to an IStream.

When i ask the TRobotStream for its IStream at the end of:

Result := TRobotStream.Create(Self as IUnknown) as IStream;

It turns around and asks its controller for the IStream interface, which triggers a call to:

Result := TRobotStream.Create(Self as IUnknown) as IStream;
   Result := TRobotStream.Create(Self as IUnknown) as IStream;

which turns around and calls:

Result := TRobotStream.Create(Self as IUnknown) as IStream;
   Result := TRobotStream.Create(Self as IUnknown) as IStream;
      Result := TRobotStream.Create(Self as IUnknown) as IStream;

Boom! Stack overflow.


Blindly, i try removing the final cast to IStream, let Delphi try to implicitely cast the object to an interface (which i just saw above doesn't work right):

Result := TRobotStream.Create(Self as IUnknown);

And now there is no crash; which i don't understand this very much. i've constructed an object, an object which supports multiple interfaces. How is it now that Delphi knows to cast the interface? Is it performing the proper reference counting? i saw above that it doesn't. Is there a subtle bug waiting to crash for the customer?

So i'm left with four possible ways to call my one line. Which one of them is valid?

  1. Result := TRobotStream.Create(Self);
  2. Result := TRobotStream.Create(Self as IUnknown);
  3. Result := TRobotStream.Create(Self) as IStream;
  4. Result := TRobotStream.Create(Self as IUnknown) as IStream;

The Real Question

i hit quite a few subtle bugs, and difficult to understand intricacies of the compiler. This leads me to believe that i have done everything completely wrong. If needed, ignore everything i said, and help me answer the question:

What is the proper way to delegate interface implementation to a child object?

Maybe i should be using TContainedObject instead of TAggregatedObject. Maybe the two work in tandem, where the parent should be TAggregatedObject and the child is TContainedObject. Maybe it's the other way around. Maybe neither apply in this case.

Note: Everything in the main part of my post can be ignored. It was just to show that i have thought about it. There are those who would argue that by including what i have tried, i have poisoned the possible answers; rather than answering my question, people might focus on my failed question.

The real goal is to delegate interface implementation to a child object. This question contains my detailed attempts at solving the problem with TAggregatedObject. You don't even see my other two solution patterns. One of which suffers from circular refernce counts, and the breaks the IUnknown equivalence rule.

Rob Kennedy might remember; and asked me to make a question that asks for a solution to the problem, rather than a solution to a problem in one of my solutions.

Edit: grammerified

Edit 2: No such thing as a robot controller. Well, there is - i worked with Funuc RJ2 controllers all the time. But not in this example!

Edit 3*

  TRobotStream = class(TAggregatedObject, IStream)
    public
        { IStream }
     function Seek(dlibMove: Largeint; dwOrigin: Longint;
        out libNewPosition: Largeint): HResult; stdcall;
     function SetSize(libNewSize: Largeint): HResult; stdcall;
     function CopyTo(stm: IStream; cb: Largeint; out cbRead: Largeint; out cbWritten: Largeint): HResult; stdcall;
     function Commit(grfCommitFlags: Longint): HResult; stdcall;
     function Revert: HResult; stdcall;
     function LockRegion(libOffset: Largeint; cb: Largeint; dwLockType: Longint): HResult; stdcall;
     function UnlockRegion(libOffset: Largeint; cb: Largeint; dwLockType: Longint): HResult; stdcall;
     function Stat(out statstg: TStatStg; grfStatFlag: Longint): HResult; stdcall;
     function Clone(out stm: IStream): HResult; stdcall;

     function Read(pv: Pointer; cb: Longint; pcbRead: PLongint): HResult; stdcall;
     function Write(pv: Pointer; cb: Longint; pcbWritten: PLongint): HResult; stdcall;
  end;

  TRobot = class(TInterfacedObject, IStream)
  private
      FStream: TRobotStream;
      function GetStream: IStream;
  public
     destructor Destroy; override;
      property Stream: IStream read GetStream implements IStream;
  end;

var
  Form1: TForm1;

implementation

{$R *.DFM}

procedure TForm1.Button1Click(Sender: TObject);
var
    rs: IStream;
begin
    rs := TRobot.Create;
    LoadRobotFromDatabase(rs); //dummy method, just to demonstrate we use the stream
    rs := nil;
end;

procedure TForm1.LoadRobotFromDatabase(rs: IStream);
begin
    rs.Revert; //dummy method call, just to prove we can call it
end;

destructor TRobot.Destroy;
begin
  FStream.Free;
  inherited;
end;

function TRobot.GetStream: IStream;
begin
  if FStream = nil then
     FStream := TRobotStream.Create(Self);
  result := FStream;
end;

Problem here is that the "parent" TRobot object is destroyed during the call to:

FStream := TRobotStream.Create(Self);

解决方案

You have to add a field instance for the created child object:

type
  TRobot = class(TInterfacedObject, IStream)
  private
     FStream: TRobotStream;
     function GetStream: IStream;
  public
     property Stream: IStream read GetStream implements IStream;
  end;

destructor TRobot.Destroy;
begin
  FStream.Free; 
  inherited; 
end;

function TRobot.GetStream: IStream;
begin
  if FStream = nil then 
    FStream := TRobotStream.Create(Self);
  result := FStream;
end;

Update TRobotStream should be derived from TAggregatedObject as you already guessed. The declaration should be:

type
  TRobotStream = class(TAggregatedObject, IStream)
   ...
  end;

It is not necessary to mention IUnknown.

In TRobot.GetStream the line result := FStream does an implicite FStream as IStream so writing this out isn't necessary either.

FStream has to be declared as TRobotStream and not as IStream so it can be destroyed when the TRobot instance is destroyed. Note: TAggregatedObject has no reference counting so the container has to take care of its lifetime.

Update (Delphi 5 code):

unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls, activex, comobj;

type
  TForm1 = class(TForm)
    Button1: TButton;
    Edit1: TEdit;
    procedure Button1Click(Sender: TObject);
  private
    procedure LoadRobotFromDatabase(rs: IStream);
  public
  end;

type
  TRobotStream = class(TAggregatedObject, IStream)
  public
    { IStream }
    function Seek(dlibMove: Largeint; dwOrigin: Longint;
       out libNewPosition: Largeint): HResult; stdcall;
    function SetSize(libNewSize: Largeint): HResult; stdcall;
    function CopyTo(stm: IStream; cb: Largeint; out cbRead: Largeint; out cbWritten: Largeint): HResult; stdcall;
    function Commit(grfCommitFlags: Longint): HResult; stdcall;
    function Revert: HResult; stdcall;
    function LockRegion(libOffset: Largeint; cb: Largeint; dwLockType: Longint): HResult; stdcall;
    function UnlockRegion(libOffset: Largeint; cb: Largeint; dwLockType: Longint): HResult; stdcall;
    function Stat(out statstg: TStatStg; grfStatFlag: Longint): HResult; stdcall;
    function Clone(out stm: IStream): HResult; stdcall;
    function Read(pv: Pointer; cb: Longint; pcbRead: PLongint): HResult; stdcall;
    function Write(pv: Pointer; cb: Longint; pcbWritten: PLongint): HResult; stdcall;
  end;

type
  TRobot = class(TInterfacedObject, IStream)
  private
    FStream: TRobotStream;
    function GetStream: IStream;
  public
    destructor Destroy; override;
    property Stream: IStream read GetStream implements IStream;
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

procedure TForm1.Button1Click(Sender: TObject);
var
  rs: IStream;
begin
  rs := TRobot.Create;
  LoadRobotFromDatabase(rs); //dummy method, just to demonstrate we use the stream
  rs := nil;
end;

procedure TForm1.LoadRobotFromDatabase(rs: IStream);
begin
  rs.Revert; //dummy method call, just to prove we can call it
end;

function TRobotStream.Clone(out stm: IStream): HResult;
begin
end;

function TRobotStream.Commit(grfCommitFlags: Integer): HResult;
begin
end;

function TRobotStream.CopyTo(stm: IStream; cb: Largeint; out cbRead, cbWritten: Largeint): HResult;
begin
end;

function TRobotStream.LockRegion(libOffset, cb: Largeint; dwLockType: Integer): HResult;
begin
end;

function TRobotStream.Read(pv: Pointer; cb: Integer; pcbRead: PLongint): HResult;
begin
end;

function TRobotStream.Revert: HResult;
begin
end;

function TRobotStream.Seek(dlibMove: Largeint; dwOrigin: Integer;
  out libNewPosition: Largeint): HResult;
begin
end;

function TRobotStream.SetSize(libNewSize: Largeint): HResult;
begin
end;

function TRobotStream.Stat(out statstg: TStatStg; grfStatFlag: Integer): HResult;
begin
end;

function TRobotStream.UnlockRegion(libOffset, cb: Largeint; dwLockType: Integer): HResult;
begin
end;

function TRobotStream.Write(pv: Pointer; cb: Integer; pcbWritten: PLongint): HResult;
begin
end;

destructor TRobot.Destroy;
begin
  FStream.Free;
  inherited;
end;

function TRobot.GetStream: IStream;
begin
  if FStream = nil then
     FStream := TRobotStream.Create(Self);
  result := FStream;
end;

end.

这篇关于Delphi:如何将接口实现委托给子对象?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆