有没有一种简单的方法可以在 C# 中模拟 Objective-C 类别?

发布于 2024-07-10 22:58:54 字数 2527 浏览 11 评论 0原文

我有一个以前从未遇到过的奇怪的设计情况...如果我使用 Objective-C,我会用类别来解决它,但我必须使用 C# 2.0。

首先,一些背景知识。 我在这个类库中有两个抽象层。 底层为扫描内容的组件实现了一个插件架构(抱歉,不能比这更具体了)。 每个插件都会以某种独特的方式进行扫描,但插件也可能因它们接受的内容类型而异。 由于与本讨论无关的各种原因,我不想通过插件接口公开泛型。 因此,我最终为每种内容类型提供了一个 IScanner 接口和一个派生接口。

顶层是一个方便的包装器,它接受包含各个部分的复合内容格式。 不同的扫描仪将需要复合的不同部分,具体取决于它们感兴趣的内容类型。因此,我需要具有特定于每个 IScanner 派生接口的逻辑,用于解析复合内容,查找所需的相关部分。

解决这个问题的一种方法是简单地向 IScanner 添加另一个方法并在每个插件中实现它。 然而,两层设计的重点是插件本身不需要了解复合格式。 解决这个问题的强力方法是在上层进行类型测试和向下转换,但是随着将来添加对新内容类型的支持,这些需要仔细维护。 在这种情况下,访问者模式也会很尴尬,因为实际上只有一个访问者,但不同可访问类型的数量只会随着时间的推移而增加(即,这些是访问者适合的相反条件)。 另外,当我真正想要的只是劫持 IScanner 的单次调度时,双次调度感觉有点矫枉过正!

如果我使用 Objective-C,我只需在每个 IScanner 派生接口上定义一个类别,并在其中添加 parseContent 方法。 类别将在上层定义,因此插件不需要更改,同时避免了类型测试的需要。 不幸的是,C# 扩展方法不起作用,因为它们基本上是静态的(即,与调用站点使用的引用的编译时类型相关,而不是像 Obj-C 类别那样挂钩到动态调度)。 更不用说,我必须使用 C# 2.0,所以我什至无法使用扩展方法。 :-P

那么在 C# 中是否有一种干净、简单的方法来解决这个问题,类似于如何使用 Objective-C 类别来解决这个问题?


编辑:一些伪代码可以帮助使当前设计的结构清晰:

interface IScanner
{ // Nothing to see here...
}

interface IContentTypeAScanner : IScanner
{
    void ScanTypeA(TypeA content);
}

interface IContentTypeBScanner : IScanner
{
    void ScanTypeB(TypeB content);
}

class CompositeScanner
{
    private readonly IScanner realScanner;

    // C-tor omitted for brevity... It takes an IScanner that was created
    // from an assembly-qualified type name using dynamic type loading.

    // NOTE: Composite is defined outside my code and completely outside my control.
    public void ScanComposite(Composite c)
    {
        // Solution I would like (imaginary syntax borrowed from Obj-C):
        // [realScanner parseAndScanContentFrom: c];
        // where parseAndScanContentFrom: is defined in a category for each
        // interface derived from IScanner.

        // Solution I am stuck with for now:
        if (realScanner is IContentTypeAScanner)
        {
            (realScanner as IContentTypeAScanner).ScanTypeA(this.parseTypeA(c));
        }
        else if (realScanner is IContentTypeBScanner)
        {
            (realScanner as IContentTypeBScanner).ScanTypeB(this.parseTypeB(c));
        }
        else
        {
            throw new SomeKindOfException();
        }
    }

    // Private parsing methods omitted for brevity...
}

编辑:为了澄清,我已经对这个设计进行了很多思考。 我有很多理由,但其中大部分我无法分享,为什么会是这样。 我还没有接受任何答案,因为虽然很有趣,但他们回避了原来的问题。

事实是,在 Obj-C 中我可以简单而优雅地解决这个问题。 问题是,我可以在 C# 中使用相同的技术吗?如果可以,如何使用? 我不介意寻找替代方案,但公平地说,这不是我问的问题。 :)

I have a weird design situation that I've never encountered before... If I were using Objective-C, I would solve it with categories, but I have to use C# 2.0.

First, some background. I have two abstraction layers in this class library. The bottom layer implements a plug-in architecture for components that scan content (sorry, can't be more specific than that). Each plug-in will do its scanning in some unique way, but also the plug-ins can vary by what type of content they accept. I didn't want to expose Generics through the plug-in interface for various reasons not relevant to this discussion. So, I ended up with an IScanner interface and a derived interface for each content type.

The top layer is a convenience wrapper that accepts a composite content format that contains various parts. Different scanners will need different parts of the composite, depending on what content type they are interested in. Therefore, I need to have logic specific to each IScanner-derived interface that parses the composite content, looking for the relevant part that is required.

One way to solve this is to simply add another method to IScanner and implement it in each plug-in. However, the whole point of the two-layer design is so that the plug-ins themselves don't need to know about the composite format. The brute-force way to solve this is by having type-tests and downcasts in the upper layer, but these would need to be carefully maintained as support for new content types is added in the future. The Visitor pattern would also be awkward in this situation because there really is only one Visitor, but the number of different Visitable types will only increase with time (i.e. -- these are the opposite conditions for which Visitor is suitable). Plus, double-dispatch feels like overkill when really all I want is to hijack IScanner's single-dispatch!

If I were using Objective-C, I would just define a category on each IScanner-derived interface and add the parseContent method there. The category would be defined in the upper layer, so the plug-ins wouldn't need to change, while simultaneously avoiding the need for type tests. Unfortunately C# extension methods wouldn't work because they are basically static (i.e. -- tied to the compile-time type of the reference used at the call site, not hooked into dynamic dispatch like Obj-C Categories). Not to mention, I have to use C# 2.0, so extension methods are not even available to me. :-P

So is there a clean and simple way to solve this problem in C#, akin to how it could be solved with Objective-C categories?


EDIT: Some pseudo-code to help make the structure of the current design clear:

interface IScanner
{ // Nothing to see here...
}

interface IContentTypeAScanner : IScanner
{
    void ScanTypeA(TypeA content);
}

interface IContentTypeBScanner : IScanner
{
    void ScanTypeB(TypeB content);
}

class CompositeScanner
{
    private readonly IScanner realScanner;

    // C-tor omitted for brevity... It takes an IScanner that was created
    // from an assembly-qualified type name using dynamic type loading.

    // NOTE: Composite is defined outside my code and completely outside my control.
    public void ScanComposite(Composite c)
    {
        // Solution I would like (imaginary syntax borrowed from Obj-C):
        // [realScanner parseAndScanContentFrom: c];
        // where parseAndScanContentFrom: is defined in a category for each
        // interface derived from IScanner.

        // Solution I am stuck with for now:
        if (realScanner is IContentTypeAScanner)
        {
            (realScanner as IContentTypeAScanner).ScanTypeA(this.parseTypeA(c));
        }
        else if (realScanner is IContentTypeBScanner)
        {
            (realScanner as IContentTypeBScanner).ScanTypeB(this.parseTypeB(c));
        }
        else
        {
            throw new SomeKindOfException();
        }
    }

    // Private parsing methods omitted for brevity...
}

EDIT: To clarify, I have thought through this design a lot already. I have many reasons, most of which I cannot share, for why it is the way it is. I have not accepted any answers yet because although interesting, they dodge the original question.

The fact is, in Obj-C I could solve this problem simply and elegantly. The question is, can I use the same technique in C# and if so, how? I don't mind looking for alternatives, but to be fair that isn't the question I asked. :)

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

森林很绿却致人迷途 2024-07-17 22:58:54

听起来你的意思是你的内容布局如下:

+--------+
| part 1 |
| type A |
+--------+
| part 2 |
| type C |
+--------+
| part 3 |
| type F |
+--------+
| part 4 |
| type D |
+--------+

并且每个部分类型都有读者。 也就是说,AScanner知道如何处理类型A的部分(如上面的部分1)中的数据,BScanner知道如何处理类型B的部分数据,等等。 到目前为止我是对的吗?

现在,如果我理解您的意思,您遇到的问题是类型读取器(IScanner 实现)不知道如何在复合容器中找到它们识别的部分。

您的复合容器能否正确枚举各个单独的部分(即,它是否知道一个部分在哪里结束,另一个部分在哪里开始),如果是这样,每个部分是否具有扫描仪或容器可以区分的某种标识?

我的意思是,数据是这样排列的吗?

+-------------+
| part 1      |
| length: 100 |
| type: "A"   |
| data: ...   |
+-------------+
| part 2      |
| length: 460 |
| type: "C"   |
| data: ...   |
+-------------+
| part 3      |
| length: 26  |
| type: "F"   |
| data: ...   |
+-------------+
| part 4      |
| length: 790 |
| type: "D"   |
| data: ...   |
+-------------+

如果您的数据布局与此类似,扫描仪是否无法请求容器的所有部件具有与给定模式匹配的标识符? 例如:

class Container : IContainer{
    IEnumerable IContainer.GetParts(string type){
        foreach(IPart part in internalPartsList)
            if(part.TypeID == type)
                yield return part;
    }
}

class AScanner : IScanner{
    void IScanner.ProcessContainer(IContainer c){
        foreach(IPart part in c.GetParts("A"))
            ProcessPart(part);
    }
}

或者,如果容器可能无法识别零件类型,但扫描仪能够识别其自己的零件类型,也许类似:

delegate void PartCallback(IPart part);

class Container : IContainer{
    void IContainer.GetParts(PartCallback callback){
        foreach(IPart part in internalPartsList)
            callback(part);
    }
}

class AScanner : IScanner{
    void IScanner.ProcessContainer(IContainer c){
        c.GetParts(delegate(IPart part){
            if(IsTypeA(part))
                ProcessPart(part);
        });
    }

    bool IsTypeA(IPart part){
        // determine if part is of type A
    }
}

也许我误解了您的内容和/或架构。 如果是这样,请澄清,我会更新。


来自OP的评论:

  1. 扫描仪不应了解容器类型。
  2. 容器类型没有内置智能。 它与您在 C# 中获得的普通旧数据非常接近。
  3. 我无法更改容器类型; 它是现有架构的一部分。

我的回复太长,无法发表评论:

  1. 扫描仪必须有某种方式来检索它们处理的部件。 如果您担心 IScanner 接口不应该识别 IContainer 接口,以便您可以自由地更改 IContainer 接口未来,那么您可以通过以下几种方式之一进行妥协:

    • 您可以向扫描仪传递一个由 IContainer 派生(或包含)的 IPartProvider 接口。 这个 IPartProvider 只会提供提供部件的功能,因此它应该非常稳定,并且可以在与 IScanner 相同的程序集中定义,以便您的插件ins 不需要引用定义了 IContainer 的程序集。
    • 您可以将委托传递给扫描仪,让他们用来检索零件。 这样,扫描器就不需要任何接口的知识(当然,IScanner 除外),只需要委托的知识。
  2. 也许您需要一个知道如何与容器和扫描仪通信的代理类。 上面提到的任何功能都可以在任何 ol' 类中实现,只要容器已经公开(或受保护地[这是一个词吗?])公开足够的功能,外部/派生类将能够访问相关数据。

从您编辑的问题中的伪代码来看,您似乎并没有真正从界面中获得任何好处,而是将插件与主应用程序紧密耦合,因为每种扫描仪类型都有 IScanner 定义了唯一的“扫描”方法,而 CompositeScanner 类对每个部件类型都有唯一的“解析”方法。

我想说这是你的首要问题。 您需要将插件(我假设是 IScanner 接口的实现者)与主应用程序分离(我假设主应用程序是 CompositeScanner 类所在的位置)。 我之前的建议之一是如何实现它,但具体细节取决于您的parseTypeX函数的工作方式。 这些可以抽象和概括吗?

据推测,您的 parseTypeX 函数与 Composite 类对象通信以获取所需的数据。 能否将这些内容移至通过 CompositeScanner 类代理的 IScanner 接口上的 Parse 方法,以从 Composite 获取此数据对象? 像这样的事情:

delegate byte[] GetDataHandler(int offset, int length);

interface IScanner{
    void   Scan(byte[] data);
    byte[] Parse(GetDataHandler getData);
}

class Composite{
    public byte[] GetData(int offset, int length){/*...*/}
}

class CompositeScanner{}
    IScanner realScanner;

    public void ScanComposite(Composite c){
        realScanner.Scan(realScanner.Parse(delegate(int offset, int length){
            return c.GetData(offset, length);
        });
    }
}

当然,可以通过删除 IScanner 上单独的 Parse 方法并将 GetDataHandler 委托直接传递给 Scan(如果需要,其实现可以调用私有Parse)。 该代码看起来与我之前的示例非常相似。

这种设计提供了我能想到的尽可能多的关注点分离和解耦。


我只是想到了其他一些你可能会觉得更容易接受的东西,而且确实可以提供更好的关注点分离。

如果您可以让每个插件向应用程序“注册”,那么您可以将解析保留在应用程序中,只要插件可以告诉应用程序如何检索其数据即可。 示例如下,但由于我不知道如何识别您的部件,因此我实现了两种可能性 - 一种用于索引部件,一种用于命名部件:

// parts identified by their offset within the file
class MainApp{
    struct BlockBounds{
        public int offset;
        public int length;

        public BlockBounds(int offset, int length){
            this.offset = offset;
            this.length = length;
        }
    }

    Dictionary<Type, BlockBounds> plugins = new Dictionary<Type, BlockBounds>();

    public void RegisterPlugin(Type type, int offset, int length){
        plugins[type] = new BlockBounds(offset, length);
    }

    public void ScanContent(Container c){
        foreach(KeyValuePair<Type, int> pair in plugins)
            ((IScanner)Activator.CreateInstance(pair.Key)).Scan(
                c.GetData(pair.Value.offset, pair.Value.length);
    }
}

或者

// parts identified by name, block length stored within content (as in diagram above)
class MainApp{
    Dictionary<string, Type> plugins = new Dictionary<string, Type>();

    public void RegisterPlugin(Type type, string partID){
        plugins[partID] = type;
    }

    public void ScanContent(Container c){
        foreach(IPart part in c.GetParts()){
            Type type;
            if(plugins.TryGetValue(part.ID, out type))
                ((IScanner)Activator.CreateInstance(type)).Scan(part.Data);
        }
    }
}

显然,我已经极大地简化了这些示例,但希望您得到这个想法。 此外,如果您可以将工厂(或工厂委托)传递给 RegisterPlugin 方法,而不是使用 Activator.CreateInstance,那就太好了。

It sounds like what you're saying is that you have content laid out something like this:

+--------+
| part 1 |
| type A |
+--------+
| part 2 |
| type C |
+--------+
| part 3 |
| type F |
+--------+
| part 4 |
| type D |
+--------+

and you have readers for each part type. That is, an AScanner knows how to process the data in a part of type A (such as part 1 above), a BScanner knows how to process the data in a part of type B, and so forth. Am I right so far?

Now, if I understand you, the problem that you're having is that the type readers (the IScanner implementations) don't know how to locate the part(s) they recognize within your composite container.

Can your composite container correctly enumerate the separate parts (i.e., does it know where one part ends and another begins), and if so, does each part have some sort of identification that either the scanner or the container can differentiate?

What I mean is, is the data laid out something like this?

+-------------+
| part 1      |
| length: 100 |
| type: "A"   |
| data: ...   |
+-------------+
| part 2      |
| length: 460 |
| type: "C"   |
| data: ...   |
+-------------+
| part 3      |
| length: 26  |
| type: "F"   |
| data: ...   |
+-------------+
| part 4      |
| length: 790 |
| type: "D"   |
| data: ...   |
+-------------+

If your data layout is similar to this, could the scanners not request of the container all parts with an identifier matching a given pattern? Something like:

class Container : IContainer{
    IEnumerable IContainer.GetParts(string type){
        foreach(IPart part in internalPartsList)
            if(part.TypeID == type)
                yield return part;
    }
}

class AScanner : IScanner{
    void IScanner.ProcessContainer(IContainer c){
        foreach(IPart part in c.GetParts("A"))
            ProcessPart(part);
    }
}

Or, if perhaps the container wouldn't be able to recognize a part type, but a scanner would be able to recognize its own part type, maybe something like:

delegate void PartCallback(IPart part);

class Container : IContainer{
    void IContainer.GetParts(PartCallback callback){
        foreach(IPart part in internalPartsList)
            callback(part);
    }
}

class AScanner : IScanner{
    void IScanner.ProcessContainer(IContainer c){
        c.GetParts(delegate(IPart part){
            if(IsTypeA(part))
                ProcessPart(part);
        });
    }

    bool IsTypeA(IPart part){
        // determine if part is of type A
    }
}

Perhaps I've misunderstood your content and/or your architecture. If so, please clarify, and I'll update.


Comment from OP:

  1. The scanners should not have any knowledge of the container type.
  2. The container type has no built-in intelligence. It is as close to plain old data as you can get in C#.
  3. I can't change the container type; It is part of an existing architecture.

My responses are too long for comments:

  1. The scanners have to have some way of retrieving the part(s) which they process. If your concern is that the IScanner interface should not be aware of the IContainer interface so that you have the freedom to alter the IContainer interface in the future, then you could compromise in one of a couple of ways:

    • You could pass the scanners an IPartProvider interface that IContainer derived from (or contained). This IPartProvider would only provide the functionality of serving up parts, so it should be pretty stable, and it could be defined in the same assembly as IScanner, so that your plug-ins wouldn't need to reference the assembly where IContainer was defined.
    • You could pass a delegate to the scanners that they could use to retrieve the parts. The scanners would then not need knowledge of any interface whatsoever (except IScanner, of course), only of the delegate.
  2. and

  3. It sounds like perhaps you need a surrogate class that knows how to communicate with both the container and the scanners. Any of the functionality mentioned above could be implemented in any ol' class, as long as the container already exposes enough functionality publicly (or protectedly [is that a word?]) that an outside/derived class would be able to access the relevant data.

From your pseudocode in your edited question, it looks like you aren't really gaining any benefit from interfaces, and are tightly coupling your plug-ins to your main app, since each scanner type has a unique derivation of IScanner that defines a unique "scan" method and the CompositeScanner class has a unique "parse" method for each part type.

I would say that this is your primary problem. You need to decouple the plug-ins—which I assume are the implementors of the IScanner interface—from the main app—which I assume is where the CompositeScanner class lives. One of my earlier suggestions is how I would implement that, but exact details depend on how your parseTypeX functions work. Can these be abstracted and generalized?

Presumably, your parseTypeX functions communicate with the Composite class object to get the data they need. Could these not be moved into a Parse method on the IScanner interface that proxied through the CompositeScanner class to get this data from the Composite object? Something like this:

delegate byte[] GetDataHandler(int offset, int length);

interface IScanner{
    void   Scan(byte[] data);
    byte[] Parse(GetDataHandler getData);
}

class Composite{
    public byte[] GetData(int offset, int length){/*...*/}
}

class CompositeScanner{}
    IScanner realScanner;

    public void ScanComposite(Composite c){
        realScanner.Scan(realScanner.Parse(delegate(int offset, int length){
            return c.GetData(offset, length);
        });
    }
}

Of course, this could be simplified by removing the separate Parse method on IScanner and simply passing the GetDataHandler delegate directly to Scan (whose implementation could call a private Parse, if desired). The code then looks very similar to my earlier examples.

This design provides as much separation of concerns and decoupling that I can think of.


I just thought of something else that you might find more palatable, and indeed, may provide better separation of concerns.

If you can have each plug-in "register" with the application, you can maybe leave parsing within the application, as long as the plug-in can tell the application how to retrieve its data. Examples are below, but since I don't know how your parts are identified, I've implemented two possibilities—one for indexed parts and one for named parts:

// parts identified by their offset within the file
class MainApp{
    struct BlockBounds{
        public int offset;
        public int length;

        public BlockBounds(int offset, int length){
            this.offset = offset;
            this.length = length;
        }
    }

    Dictionary<Type, BlockBounds> plugins = new Dictionary<Type, BlockBounds>();

    public void RegisterPlugin(Type type, int offset, int length){
        plugins[type] = new BlockBounds(offset, length);
    }

    public void ScanContent(Container c){
        foreach(KeyValuePair<Type, int> pair in plugins)
            ((IScanner)Activator.CreateInstance(pair.Key)).Scan(
                c.GetData(pair.Value.offset, pair.Value.length);
    }
}

or

// parts identified by name, block length stored within content (as in diagram above)
class MainApp{
    Dictionary<string, Type> plugins = new Dictionary<string, Type>();

    public void RegisterPlugin(Type type, string partID){
        plugins[partID] = type;
    }

    public void ScanContent(Container c){
        foreach(IPart part in c.GetParts()){
            Type type;
            if(plugins.TryGetValue(part.ID, out type))
                ((IScanner)Activator.CreateInstance(type)).Scan(part.Data);
        }
    }
}

Obviously, I've extremely simplified these examples, but hopefully, you get the idea. Additionally, rather than using Activator.CreateInstance, it would be nice if you could pass a factory (or a factory delegate) to the RegisterPlugin method.

我一直都在从未离去 2024-07-17 22:58:54

我要尝试...;-)
如果在您的系统中存在一个填充 IScanner 对象“目录”的阶段,您可以考虑用一个属性来装饰您的 IScanner ,该属性说明了哪个 Part< /code> 他们感兴趣。然后您可以映射此信息并使用地图驱动您的 Composite 的扫描。
这不是一个完整的答案:如果我有一点时间,我会尝试详细说明...

编辑:一些伪代码来支持我的混乱解释

public interface IScanner
{
    void Scan(IPart part);
}

public interface IPart
{
    string ID { get; }
}

[ScannedPart("your-id-for-A")]
public class AlphaScanner : IScanner
{
    public void Scan(IPart part)
    {
        throw new NotImplementedException();
    }
}

[ScannedPart("your-id-for-B")]
public class BetaScanner : IScanner
{
    public void Scan(IPart part)
    {
        throw new NotImplementedException();
    }
}

public interface IComposite
{
    List<IPart> Parts { get; }
}

public class ScannerDriver
{
    public Dictionary<string, IScanner> Scanners { get; private set; }

    public void DoScan(IComposite composite)
    {
        foreach (IPart part in composite.Parts)
        {
            IScanner scanner = Scanners[part.ID];
            scanner.Scan(part);
        }
    }
}

不要按原样接受:它是为了解释目的。

编辑:回答内核上校的评论。
我很高兴你觉得这很有趣。 :-)
在这个简单的代码草图中,应该仅在字典初始化期间(或需要时)涉及反射,并且在此阶段您可以“强制”属性的存在(甚至使用映射扫描仪和部件的其他方式)。 我说“强制”是因为,即使它不是编译时约束,我认为您在将代码投入生产之前至少会运行一次代码;-) 所以它可能是一次运行- 如果需要的话,时间限制。 我想说,灵感是类似于 MEF 或其他类似框架的东西(非常非常简单)。
只是我的2分钱。

I'm going to try... ;-)
If in your system there is a phase when you populate your "catalog" of IScanner objects you could think of decorating your IScanners with an attribute stating which Part they are interested in. Then you can map this information and drive the scanning of your Composite with the map.
This is not a full answer: if I have a bit of time I'll try to elaborate...

Edit: a bit of pseudo-code to support my confused explanation

public interface IScanner
{
    void Scan(IPart part);
}

public interface IPart
{
    string ID { get; }
}

[ScannedPart("your-id-for-A")]
public class AlphaScanner : IScanner
{
    public void Scan(IPart part)
    {
        throw new NotImplementedException();
    }
}

[ScannedPart("your-id-for-B")]
public class BetaScanner : IScanner
{
    public void Scan(IPart part)
    {
        throw new NotImplementedException();
    }
}

public interface IComposite
{
    List<IPart> Parts { get; }
}

public class ScannerDriver
{
    public Dictionary<string, IScanner> Scanners { get; private set; }

    public void DoScan(IComposite composite)
    {
        foreach (IPart part in composite.Parts)
        {
            IScanner scanner = Scanners[part.ID];
            scanner.Scan(part);
        }
    }
}

Don't take it as-is: it's for explanation purpose.

Edit: answer to Colonel Kernel comments.
I'm glad you find it interesting. :-)
In this simple sketch of code reflection should be involved just during the Dictionary initialization (or when needed) and during this phase you can "enforce" the presence of the attribute (or even use other ways of mapping scanners and parts). I say "enforce" because, even if it isn't a compile-time constraint, I think you will run your code at least once before putting it into production ;-) so it could be a run-time constraint if needed. I would say that the inspiration is something (very very lightly) similar to MEF or other similar frameworks.
Just my 2 cents.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文